1. プログラム、プロセス、スレッド
プログラムは、順序付けられた命令の集合であり、一般に、数行の命令として理解されることもあります。コード。それ自体には実行する意味はなく、単なる静的なエンティティであり、単純なテキスト ファイルである場合もあれば、コンパイル後に生成される実行可能ファイルである場合もあります。
狭義には、プロセスは実行中のプログラムのインスタンスであり、広義には、プロセスは、特定のデータ収集に対して特定の独立した機能を備えたプログラムの実行アクティビティです。プロセスは、オペレーティング システムによるリソース割り当ての基本単位です。
スレッドは、独立して実行できるプロセスの最小単位であり、プロセッサによる独立したスケジューリングとディスパッチの基本単位でもあります。プロセスには複数のスレッドを含めることができ、各スレッドは独自のタスクを実行し、同じプロセス内のすべてのスレッドはプロセス内のリソース (メモリ空間、ファイル ハンドルなど) を共有します。
#2. マルチスレッド プログラミングの概要
1. マルチスレッド プログラミングとは
マルチ スレッド プログラミング テクノロジは、Java 言語の重要な機能です。マルチスレッド プログラミングの意味は、プログラム タスクをいくつかの並列サブタスクに分割し、これらのサブタスクを複数のスレッドに割り当てて実行することです。 マルチスレッド プログラミングは、スレッドを基本的な抽象単位とするプログラミング パラダイムです。ただし、マルチスレッド プログラミングは、プログラミングに複数のスレッドを使用するという単純なだけではなく、解決する必要がある独自の問題もあります。マルチスレッドプログラミングとオブジェクト指向プログラミングは互換性があり、オブジェクト指向プログラミングをベースにしてマルチスレッドプログラミングを実装できます。実際、Java プラットフォームのスレッドはオブジェクトです。
2. なぜマルチスレッド プログラミングを使用する必要があるのですか?
今日のコンピューターにはマルチプロセッサ コアが搭載されていることが多く、各スレッドは同時に 1 つのプロセッサ上でのみ実行できます。時間的に優れています。シングルスレッドのみで開発を行った場合、マルチコアプロセッサのリソースを十分に活用してプログラムの実行効率を向上させることができません。プログラミングにマルチスレッドを使用する場合、異なるプロセッサ上で異なるスレッドを実行できます。このようにして、コンピュータリソースの使用率が大幅に向上するだけでなく、プログラムの実行効率も向上します。
3. JAVA スレッド API の概要
java.lang.Thread クラスは、Java プラットフォームによるスレッドの実装です。 Thread クラスまたはそのサブクラスのインスタンスがスレッドです。
1. スレッドの作成、起動、実行
Java プラットフォームでは、スレッドの作成は Thread クラス (またはそのサブクラス) の作成の例です。各スレッドには実行するタスクがあります。スレッドのタスク処理ロジックは、Thread クラスの run メソッドに直接実装することも、このメソッドを通じて呼び出すこともできます。したがって、run メソッドは、スレッドのタスク処理ロジックのエントリ メソッドに相当します。Java 仮想関数によって直接呼び出す必要があります。対応するスレッドを実行するときにマシンを呼び出します。アプリケーション コードによって呼び出されるべきではありません。 スレッドを実行するとは、実際には、Java 仮想マシンにスレッドの run メソッドを実行させ、タスク処理ロジック コードを実行できるようにすることを意味します。スレッドが開始されていない場合、その run メソッドは決して実行されません。これを行うには、まずスレッドを開始する必要があります。 Thread クラスの start メソッドは、対応するスレッドを開始するために使用されます。スレッドの開始の本質は、仮想マシンに対応するスレッドの実行を要求することです。このスレッドがいつ実行できるかは、スレッド スケジューラによって決定されます (スレッド スケジューラはオペレーティング システムの一部です)。したがって、スレッドの start メソッドを呼び出しても、スレッドが実行を開始したことを意味するわけではなく、スレッドはすぐに実行を開始する場合もあれば、後で実行される場合もあれば、まったく実行されない場合もあります。
以下ではスレッドを立てる方法を 2 つ紹介します(実は他にも方法があります。詳しくは次回以降の記事で紹介します)。その前に、Thread クラスの run メソッドのソース コードを見てみましょう:// Code 1-1@Override public void run() { if (target != null) { target.run(); } }
この run メソッドは Runnable インターフェイスで定義されており、パラメータを受け入れず、戻り値もありません。実際、Runnable インターフェイスにはメソッドが 1 つだけあるため、このインターフェイスは関数型インターフェイスです。つまり、Runnable が必要な場合はラムダ式を使用できます。 Thread クラスはこのインターフェイスを実装するため、このメソッドを実装する必要があります。 target は Thread クラスのフィールドであり、その型も Runnable です。 target フィールドは、このスレッドが何を実行する必要があるかを示し、Thread クラスの run メソッドはターゲットの run メソッドのみを実行します。
Java 仮想マシンがスレッドの run メソッドを自動的に呼び出すと先ほど述べました。ただし、Thread クラスの run メソッドは定義されているため、実行する必要があるコードを Thread クラスの run メソッドに記述する方法がありません。したがって、run メソッドの動作に影響を与える他の方法を検討できます。 1 つ目は、Thread クラスを継承して run メソッドをオーバーライドし、JVM がスレッドの実行時に Thread クラスの run メソッドの代わりにオーバーライドされた run メソッドを呼び出すようにする方法です。2 つ目の方法は、実行するコードを渡すことです。 Thread クラスのターゲット メソッドであり、Thread クラスにはターゲットに値を直接割り当てることができるいくつかのコンストラクターがあるため、JVM は run メソッドの呼び出し時に渡したコードを引き続き実行します。
Java プラットフォームでは、各スレッドに独自のデフォルト名を付けることができます。もちろん、Thread クラスのインスタンスを構築するときにスレッドに名前を付けることもできます。この名前を使用すると、さまざまなスレッドを区別しやすくなります。
次のコードは、上記の 2 つのメソッドを使用して 2 つのスレッドを作成します。実行する必要があるタスクは非常に単純です - ウェルカム メッセージを 1 行出力し、自分の名前を含めます。
public class WelcomeApp { public static void main(String[] args) { Thread thread1 = new WelcomeThread(); Thread thread2 = new Thread(() -> System.out.println("2. Welcome, I'm " + Thread.currentThread().getName())); thread1.start(); thread2.start(); } }class WelcomeThread extends Thread { @Override public void run() { System.out.println("1. Welcome, I'm " + Thread.currentThread().getName()); } }
このプログラムを実行したときの出力は次のとおりです:
1. Welcome, I'm Thread-0 2. Welcome, I'm Thread-1
このプログラムを複数回実行すると、このプログラムの出力も次のようになることがあることがわかります:
2. Welcome, I'm Thread-1 1. Welcome, I'm Thread-0
これは、スレッド 1 がスレッド 2 より前に開始されても、スレッド 1 がスレッド 2 より前に実行されることを意味しないことを示しています。
どのメソッドでスレッドを作成しても、スレッドの run メソッド(JVM によって呼び出される)の実行が終了すると、対応するスレッドの実行も終了します。もちろん、runメソッドの実行終了には、正常終了(runメソッドが正常に戻る)や、コード中にスローされた例外による終了も含まれます。実行を終了したスレッドが占有していたリソース (メモリ空間など) は、他の Java オブジェクトと同様に JVM によってリサイクルされます。
スレッドは「使い捨てアイテム」であり、実行が終了したスレッドをstartメソッドを再度呼び出して再実行することはできません。実際、start メソッドは 1 回しか呼び出すことができず、同じ Thread インスタンスの start メソッドを複数回呼び出すと、IllegalThreadStateException 例外がスローされます。
2. スレッド属性
スレッド属性には、スレッド番号、名前、カテゴリ、優先度が含まれ、詳細は次の表に示すとおりです。
もちろん、Linux システム上で kill コマンドを使用して Java 仮想マシンのプロセスを強制終了するなど、Java 仮想マシンが強制的に停止された場合は、ユーザー スレッドでも Java 仮想マシンの停止を防ぐことはできません。
join メソッドの機能は、メソッドを実行するスレッドと同等であり、スレッド スケジューラは「最初に一時停止する必要があり、他のスレッドの実行が終了するまで続行できません。」と指示します。 yield 静的メソッドは実行と同等です。このメソッドのスレッドはスレッド スケジューラに次のように指示します。「今は急いでいません。他の人がプロセッサ リソースを必要とする場合は、最初にそれを使用させてください。もちろん、誰も使用しない場合は、
sleep 静的メソッドの役割は、メソッドを実行するスレッドがスレッド スケジューラに次のように指示することに相当します。しばらくしたら起きて仕事を続けてください。」
4、Thread类中的废弃方法
虽然这些方法并没有相应的替代品,但是可以使用其他办法来实现,我们会在后续文章中学习这部分内容。
四、无处不在的线程
Java平台本身就是一个多线程的平台。除了Java开发人员自己创建和使用的线程,Java平台中其他由Java虚拟机创建、使用的线程也随处可见。当然,这些线程也是各自有其处理任务。
Java虚拟机启动的时候会创建一个主线程(main线程),该线程负责执行Java程序的入口方法(main方法)。下面的程序打印出主线程的名称:
public class MainThreadDemo { public static void main(String[] args) { System.out.println(Thread.currentThread().getName()); } }
该程序会输出“main”,这说明main方法是由一个名为“main”的线程调用的,这个线程就是主线程,它是由JVM创建并启动的。
在多线程编程中,弄清楚一段代码具体是由哪个(或者哪种)线程去负责执行的这点很重要,这关系到性能、线程安全等问题。本系列的后续文章会体现这点。
Java 虚拟机垃圾回收器(Garbage Collector)负责对Java程序中不再使用的内存空间进行回收,而这个回收的动作实际上也是通过专门的线程(垃圾回收线程)实现的,这些线程由Java虚拟机自行创建。
为了提高Java代码的执行效率,Java虚拟机中的JIT(Just In Time)编译器会动态地将Java字节码编译为Java虚拟机宿主机处理器可直接执行的机器码。这个动态编译的过程实际上是由Java虚拟机创建的专门的线程负责执行的。
Java平台中的线程随处可见,这些线程各自都有其处理任务。
五、线程的层次关系
Java平台中的线程不是孤立的,线程与线程之间总是存在一些联系。假设线程A所执行的代码创建了线程B, 那么,习惯上我们称线程B为线程A的子线程,相应地线程A就被称为线程B的父线程。例如, Code 1-2中的线程thread1和thread2是main线程的子线程,main线程是它们的父线程。子线程所执行的代码还可以创建其他线程,因此一个子线程也可以是其他线程的父线程。所以,父线程、子线程是一个相对的称呼。理解线程的层次关系有助于我们理解Java应用程序的结构,也有助于我们后续阐述其他概念。
在Java平台中,一个线程是否是一个守护线程默认取决于其父线程:默认情况下父线程是守护线程,则子线程也是守护线程;父线程是用户线程,则子线程也是用户线程。另外,父线程在创建子线程后启动子线程之前可以调用该线程的setDaemon方法,将相应的线程设置为守护线程(或者用户线程)。
一个线程的优先级默认值为该线程的父线程的优先级,即如果我们没有设置或者更改一个线程的优先级,那么这个线程的优先级的值与父线程的优先级的值相等。
不过,Java平台中并没有API用于获取一个线程的父线程,或者获取一个线程的所有子线程。并且,父线程和子线程之间的生命周期也没有必然的联系。比如父线程运行结束后,子线程可以继续运行,子线程运行结束也不妨碍其父线程继续运行。
六、线程的生命周期状态
在Java平台中,一个线程从其创建、启动到其运行结束的整个生命周期可能经历若干状态。如下图所示:
スレッドのステータスは、Thread.getState() を呼び出すことで取得できます。 Thread.getState() の戻り値の型は Thread.State で、Thread クラス内の列挙型です。 Thread.State で定義されるスレッドの状態には次のものがあります。
NEW
: 作成されたが開始されていないスレッドはこの状態になります。スレッド インスタンスは 1 回しか開始できないため、スレッドがこの状態になるのは 1 回だけです。
- RUNNABLE
: この状態は、READY と RUNNING の 2 つのサブ状態を含む複合状態と見なすことができますが、実際には、これら 2 つの状態は Thread.State で定義されていません。前者は、この状態のスレッドをスレッド スケジューラによってスケジュールして、実行状態にすることができることを意味します。後者は、この状態のスレッドが実行中であること、つまり、対応するスレッド オブジェクトの run メソッドに対応する命令がプロセッサによって実行されていることを示します。 Thread.yield() を実行しているスレッドのステータスは、RUNNING から READY に変換される可能性があります。 READY サブ状態にあるスレッドは、アクティブ スレッドとも呼ばれます。
BLOCKED
: スレッドがブロッキング I/0 操作を開始した後、または他のスレッドが保持する排他的リソース (ロックなど) を申請した後、対応するスレッドはこの状態になります。 BLOCKED 状態のスレッドはプロセッサ リソースを占有しません。ブロッキング 1/0 操作が完了するか、スレッドが要求したリソースを取得すると、スレッドのステータスを RUNNABLE に変換できます。
WAITING
: スレッドがいくつかの特定のメソッドを実行した後、他のスレッドが他の特定の操作を実行するのを待機するこの状態になります。実行スレッドを WAITING 状態に変更できるメソッドには、Object.wait()、Thread.join()、および LockSupport.park(Object) があります。対応するスレッドを待機中から実行可能に変更できる対応するメソッドには、Object.notify()/notifyAll() および LockSupport.unpark(Object)) が含まれます。
TIMED_WAITING
: この状態は WAITING に似ていますが、異なる点は、この状態のスレッドは他のスレッドが特定の操作を実行するのを無期限に待機するのではなく、時間制限のある待機状態であることです。他のスレッドが、指定された時間内にそのスレッドが期待する特定の操作を実行しない場合、スレッドのステータスは自動的に RUNNABLE に変換されます。
TERMINATED
: 実行を終了したスレッドはこの状態です。スレッド インスタンスは 1 回しか開始できないため、スレッドがこの状態になるのは 1 回だけです。 run メソッドは正常に戻るか、例外をスローするために早期に終了します。これにより、対応するスレッドがこの状態になります。
スレッドが NEW 状態および TERMINATED 状態になることは、そのライフサイクル全体で 1 回だけです。
7. マルチスレッド プログラミングの利点
マルチスレッド プログラミングには次の利点があります:
システムの効率の向上 スループット レート: マルチスレッド プログラミングにより、1 つのプロセスで複数の同時 (つまり、同時) 操作が可能になります。たとえば、1 つのスレッドが I/0 操作を待機している間、他のスレッドは引き続き操作を実行できます。
応答性の向上: マルチスレッド プログラミングを使用する場合、GUI ソフトウェア (デスクトップ アプリケーションなど) の場合、操作が遅い (サーバーから大きなファイルをダウンロードするなど) ことが原因でソフトウェアのインターフェイスが表示されなくなります。 「フリーズ」され、他のユーザー操作に応答できなくなります。Web アプリケーションの場合、1 つのリクエストの処理が遅くても、他のリクエストの処理には影響しません。
マルチコア プロセッサ リソースを最大限に活用する: 現在、マルチコア プロセッサ デバイスの人気が高まっており、携帯電話などのコンシューマ デバイスでもマルチコア プロセッサが一般的に使用されています。適切なマルチスレッド プログラミングを実装すると、デバイスのマルチコア プロセッサ リソースを最大限に活用し、リソースの無駄を避けることができます。
マルチスレッド プログラミングには、次の側面を含む独自の問題とリスクもあります。
スレッドの安全性の問題。複数のスレッドがデータを共有する場合、対応する同時アクセス制御手段が講じられていない場合、ダーティ データ (期限切れのデータ) の読み取りや更新の損失 (一部のスレッドによって行われた更新が他のスレッドによって削除される) などのデータ整合性の問題が発生する可能性があります。スレッドは上書きされます)など。
スレッドアクティビティの問題。スレッドの作成から実行の終了までのライフサイクル全体は、さまざまな状態を経ます。単一スレッドの観点からは、RUNNABLE 状態が望ましい状態です。しかし、実際には、コードの書き方を誤ると、一部のスレッドが他のスレッドのロック解除を待っている状態(BLOCKED 状態)になることがあり、この状態をデッドロック(Deadlock)といいます。もちろん、常にビジー状態のスレッドでも問題が発生する可能性があり、ライブロックの問題、つまりスレッドが操作を試行しているが処理を進められない問題が発生する可能性があります。さらに、スレッドは希少なコンピューティング リソースであり、システムに搭載されているプロセッサの数は、システム内に存在するスレッドの数に比べて常に非常に少ないです。場合によっては、スレッド スターベーション (Starvation) の問題が発生することがあります。つまり、一部のスレッドはプロセッサによって実行される機会が得られず、常に RUNNABLE 状態の READY サブ状態になります。
コンテキストの切り替え。プロセッサが 1 つのスレッドの実行から別のスレッドの実行に切り替えるとき、オペレーティング システムが必要とするアクションはコンテキスト スイッチと呼ばれます。プロセッサ リソースが不足しているため、コンテキストの切り替えはマルチスレッド プログラミングの避けられない副産物とみなされ、システムの消費量が増加し、システムのスループットに貢献しません。
関連する問題の詳細については、PHP 中国語 Web サイトを参照してください: JAVA ビデオ チュートリアル
以上がJAVA のマルチスレッド プログラミング手法の詳細な分析 (例付き)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。