Java 101:Javaスレッドについて、パート3:スレッドのスケジューリングと待機/通知

今月は、スレッドのスケジューリング、待機/通知メカニズム、およびスレッドの中断に焦点を当てて、Javaスレッドの4部構成の紹介を続けます。 JVMまたはオペレーティングシステムのスレッドスケジューラが実行する次のスレッドをどのように選択するかを調査します。お気づきのように、優先順位はスレッドスケジューラの選択にとって重要です。スレッドが実行を続行する前に別のスレッドから通知を受信するまで待機する方法を調べ、生産者/消費者関係で2つのスレッドの実行を調整するための待機/通知メカニズムの使用方法を学習します。最後に、スレッドの終了やその他のタスクのために、スリープ中のスレッドまたは待機中のスレッドを時期尚早に目覚めさせる方法を学習します。また、スリープも待機もしていないスレッドが別のスレッドからの割り込み要求を検出する方法についても説明します。

この記事(JavaWorldアーカイブの一部)は、2013年5月に新しいコードリストとダウンロード可能なソースコードで更新されたことに注意してください。

Javaスレッドを理解する-シリーズ全体を読む

  • パート1:スレッドとランナブルの紹介
  • パート2:同期
  • パート3:スレッドのスケジューリング、待機/通知、およびスレッドの中断
  • パート4:スレッドグループ、ボラティリティ、スレッドローカル変数、タイマー、スレッドの停止

スレッドスケジューリング

理想的な世界では、すべてのプログラムスレッドには、実行する独自のプロセッサがあります。コンピュータに数千または数百万のプロセッサが搭載される時が来るまで、スレッドは多くの場合、1つ以上のプロセッサを共有する必要があります。JVMまたは基盤となるプラットフォームのオペレーティングシステムのいずれかが、スレッド間でプロセッサリソースを共有する方法を解読します。これはスレッドスケジューリングと呼ばれるタスクです。スレッドスケジューリングを実行するJVMまたはオペレーティングシステムのその部分は、スレッドスケジューラです。

注:スレッドスケジューリングの説明を簡略化するために、単一プロセッサのコンテキストでのスレッドスケジューリングに焦点を当てています。この議論を複数のプロセッサに外挿することができます。その仕事はあなたに任せます。

スレッドのスケジューリングに関する2つの重要なポイントを覚えておいてください。

  1. Javaは、VMに特定の方法でスレッドをスケジュールしたり、スレッドスケジューラを含めたりすることを強制しません。これは、プラットフォームに依存するスレッドスケジューリングを意味します。したがって、動作がスレッドのスケジュール方法に依存し、さまざまなプラットフォーム間で一貫して動作する必要があるJavaプログラムを作成する場合は、注意が必要です。
  2. 幸い、Javaプログラムを作成するときは、プログラムのスレッドの少なくとも1つがプロセッサを長期間頻繁に使用し、そのスレッドの実行の中間結果が重要であることが判明した場合にのみ、Javaがスレッドをスケジュールする方法について考える必要があります。たとえば、アプレットには、画像を動的に作成するスレッドが含まれています。定期的に、ペイントスレッドでその画像の現在のコンテンツを描画して、ユーザーが画像の進行状況を確認できるようにします。計算スレッドがプロセッサを独占しないようにするには、スレッドのスケジューリングを検討してください。

2つのプロセッサを集中的に使用するスレッドを作成するプログラムを調べます。

リスト1.SchedDemo.java

// SchedDemo.java class SchedDemo { public static void main (String [] args) { new CalcThread ("CalcThread A").start (); new CalcThread ("CalcThread B").start (); } } class CalcThread extends Thread { CalcThread (String name) { // Pass name to Thread layer. super (name); } double calcPI () { boolean negative = true; double pi = 0.0; for (int i = 3; i < 100000; i += 2) { if (negative) pi -= (1.0 / i); else pi += (1.0 / i); negative = !negative; } pi += 1.0; pi *= 4.0; return pi; } public void run () { for (int i = 0; i < 5; i++) System.out.println (getName () + ": " + calcPI ()); } }

SchedDemoそれぞれがpiの値を計算し(5回)、各結果を出力する2つのスレッドを作成します。JVM実装がスレッドをスケジュールする方法によっては、次のような出力が表示される場合があります。

CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894

上記の出力によると、スレッドスケジューラは両方のスレッド間でプロセッサを共有します。ただし、次のような出力が表示されます。

CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread A: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894 CalcThread B: 3.1415726535897894

上記の出力は、あるスレッドを別のスレッドよりも優先するスレッドスケジューラを示しています。上記の2つの出力は、スレッドスケジューラの2つの一般的なカテゴリ(緑とネイティブ)を示しています。次のセクションでは、動作の違いについて説明します。各カテゴリについて説明する際に、スレッドの状態について説明します。スレッドの状態には次の4つがあります。

  1. 初期状態:プログラムがスレッドのスレッドオブジェクトを作成しましたが、スレッドオブジェクトのstart()メソッドがまだ呼び出されていないため、スレッドはまだ存在していません。
  2. 実行可能状態:これはスレッドのデフォルト状態です。の呼び出しがstart()完了すると、スレッドが実行されているかどうかに関係なく、つまりプロセッサを使用して、スレッドが実行可能になります。多くのスレッドが実行可能である可能性がありますが、現在実行されているのは1つだけです。スレッドスケジューラは、プロセッサに割り当てる実行可能なスレッドを決定します。
  3. 閉塞状態:スレッドが実行されるとsleep()wait()またはjoin()スレッドの試みがネットワークからまだ利用可能でないデータを読み出すときに、方法を、スレッドが待機ロックを取得するときに、そのスレッドがブロック状態にある:それはどちらも実行されていないも実行する位置に。(スレッドが何かが起こるのを待つ他の時を考えることができるでしょう。)ブロックされたスレッドがブロックを解除すると、そのスレッドは実行可能状態に移行します。
  4. 終了状態:実行がスレッドのrun()メソッドを離れると、そのスレッドは終了状態になります。つまり、スレッドは存在しなくなります。

スレッドスケジューラは、実行する実行可能なスレッドをどのように選択しますか?グリーンスレッドのスケジューリングについて話し合いながら、その質問に答え始めます。ネイティブスレッドのスケジューリングについて話し合いながら、答えを終えます。

グリーンスレッドスケジューリング

すべてのオペレーティングシステム、たとえば、古代のMicrosoft Windows3.1実行システムがスレッドをサポートしているわけではありません。このようなシステムの場合、Sun Microsystemsは、実行の唯一のスレッドを複数のスレッドに分割するJVMを設計できます。JVM(基盤となるプラットフォームのオペレーティングシステムではない)は、スレッドロジックを提供し、スレッドスケジューラを含みます。JVMスレッドは、グリーンスレッドまたはユーザースレッドです。

JVMのスレッドスケジューラは、優先度(スレッドの相対的な重要度)に従ってグリーンスレッドをスケジュールします。これは、明確に定義された値の範囲から整数として表現されます。通常、JVMのスレッドスケジューラは最も優先度の高いスレッドを選択し、そのスレッドを終了またはブロックするまで実行できるようにします。その際、スレッドスケジューラは次に優先度の高いスレッドを選択します。そのスレッドは(通常)終了またはブロックするまで実行されます。スレッドの実行中に、優先度の高いスレッドのブロックが解除された場合(おそらく、優先度の高いスレッドのスリープ時間が経過した場合)、スレッドスケジューラは優先度の低いスレッドをプリエンプトまたは割り込み、ブロックされていない優先度の高いスレッドをプロセッサに割り当てます。

注:優先度が最も高い実行可能スレッドが常に実行されるとは限りません。ここだJava言語仕様」の優先順位を取ります:

すべてのスレッドに優先順位があり ます。リソースの処理で競合が発生する場合、通常、優先度の高いスレッドが優先度の低いスレッドよりも優先して実行されます。ただし、このような優先順位は、最も優先度の高いスレッドが常に実行されることを保証するものではなく、スレッドの優先順位を使用して相互排除を確実に実装することはできません。

その承認は、グリーンスレッドJVMの実装について多くを語っています。これらのJVMは、JVMの唯一の実行スレッドを拘束するため、スレッドをブロックさせる余裕がありません。したがって、スレッドがファイルからの到着に時間がかかるデータの読み取りを行っている場合など、スレッドがブロックする必要がある場合、JVMはスレッドの実行を停止し、ポーリングメカニズムを使用してデータの到着を決定する場合があります。スレッドが停止したままの場合、JVMのスレッドスケジューラは優先度の低いスレッドの実行をスケジュールする可能性があります。優先度の低いスレッドの実行中にデータが到着するとします。優先度の高いスレッドはデータが到着するとすぐに実行されるはずですが、それはJVMが次にオペレーティングシステムをポーリングして到着を検出するまで実行されません。したがって、優先度の高いスレッドを実行する必要がある場合でも、優先度の低いスレッドが実行されます。Javaからのリアルタイムの動作が必要な場合にのみ、この状況について心配する必要があります。しかし、Javaはリアルタイムオペレーティングシステムではないので、なぜ心配するのでしょうか。

どの実行可能なグリーンスレッドが現在実行中のグリーンスレッドになるかを理解するには、次のことを考慮してください。アプリケーションが、main()メソッドを実行するメインスレッド、計算スレッド、およびキーボード入力を読み取るスレッドの3つのスレッドで構成されているとします。キーボード入力がない場合、読み取りスレッドはブロックされます。読み取りスレッドの優先度が最も高く、計算スレッドの優先度が最も低いと想定します。 (簡単にするために、他の内部JVMスレッドが使用できないことも想定してください。)図1は、これら3つのスレッドの実行を示しています。

時間T0で、メインスレッドが実行を開始します。時間T1で、メインスレッドは計算スレッドを開始します。計算スレッドはメインスレッドよりも優先度が低いため、計算スレッドはプロセッサを待機します。時間T2で、メインスレッドは読み取りスレッドを開始します。読み取りスレッドはメインスレッドよりも優先度が高いため、読み取りスレッドの実行中、メインスレッドはプロセッサを待機します。時間T3で、読み取りスレッドがブロックされ、メインスレッドが実行されます。時間T4で、読み取りスレッドはブロックを解除して実行します。メインスレッドは待機します。最後に、時間T5で、読み取りスレッドがブロックされ、メインスレッドが実行されます。読み取りスレッドとメインスレッドの間のこの実行の交代は、プログラムが実行されている限り継続します。計算スレッドは優先度が最も低く、プロセッサの注意が不足しているため、実行されません。として知られている状況プロセッサの枯渇

計算スレッドにメインスレッドと同じ優先順位を与えることで、このシナリオを変更できます。図2は、時間T2から始まる結果を示しています。(T2より前の図2は図1と同じです。)

At time T2, the reading thread runs while the main and calculation threads wait for the processor. At time T3, the reading thread blocks and the calculation thread runs, because the main thread ran just before the reading thread. At time T4, the reading thread unblocks and runs; the main and calculation threads wait. At time T5, the reading thread blocks and the main thread runs, because the calculation thread ran just before the reading thread. This alternation in execution between the main and calculation threads continues as long as the program runs and depends on the higher-priority thread running and blocking.

We must consider one last item in green thread scheduling. What happens when a lower-priority thread holds a lock that a higher-priority thread requires? The higher-priority thread blocks because it cannot get the lock, which implies that the higher-priority thread effectively has the same priority as the lower-priority thread. For example, a priority 6 thread attempts to acquire a lock that a priority 3 thread holds. Because the priority 6 thread must wait until it can acquire the lock, the priority 6 thread ends up with a 3 priority—a phenomenon known as priority inversion.

優先順位の逆転は、優先順位の高いスレッドの実行を大幅に遅らせる可能性があります。たとえば、優先度3、4、および9の3つのスレッドがあるとします。優先度3のスレッドが実行されており、他のスレッドはブロックされています。優先度3のスレッドがロックを取得し、優先度4のスレッドがブロックを解除するとします。優先度4のスレッドが現在実行中のスレッドになります。優先度9のスレッドにはロックが必要なため、優先度3のスレッドがロックを解除するまで待機し続けます。ただし、優先度3のスレッドは、優先度4のスレッドがブロックまたは終了するまでロックを解放できません。その結果、優先度9のスレッドは実行を遅らせます。