実世界でのJavaスレッドのプログラミング、パート1

単純なコンソールベースのアプリケーション以外のすべてのJavaプログラムは、好むと好まざるとにかかわらず、マルチスレッド化されています。問題は、Abstract Windowing Toolkit(AWT)が独自のスレッドでオペレーティングシステム(OS)イベントを処理するため、リスナーメソッドが実際にAWTスレッドで実行されることです。これらの同じリスナーメソッドは通常、メインスレッドからもアクセスされるオブジェクトにアクセスします。この時点で、頭を砂に埋めて、スレッドの問題を心配する必要がないふりをしたくなるかもしれませんが、通常はそれを回避することはできません。そして、残念ながら、Javaに関する本のどれも、スレッドの問題を十分に深く扱っていません。(このトピックに関する役立つ本のリストについては、「参考文献」を参照してください。)

この記事は、マルチスレッド環境でのJavaプログラミングの問題に対する実際の解決策を紹介するシリーズの最初の記事です。これは、言語レベルのもの(synchronizedキーワードとThreadクラスのさまざまな機能)を理解しているが、これらの言語機能を効果的に使用する方法を学びたいJavaプログラマーを対象としています。

プラットフォームへの依存

残念ながら、Javaのプラットフォームの独立性の約束は、スレッドの分野では一線を画しています。プラットフォームに依存しないマルチスレッドJavaプログラムを作成することは可能ですが、目を開いて作成する必要があります。これは実際にはJavaのせいではありません。真にプラットフォームに依存しないスレッドシステムを作成することはほとんど不可能です。(DougSchmidtのACE [Adaptive Communication Environment]フレームワークは、複雑ではありますが、優れた試みです。彼のプログラムへのリンクについては、「参考文献」を参照してください。)したがって、以降の記事でハードコアJavaプログラミングの問題について話す前に、 Java仮想マシン(JVM)が実行される可能性のあるプラットフォームによってもたらされる問題について話し合います。

原子力

理解することが重要な最初のOSレベルの概念は、アトミック性です。アトミック操作は、別のスレッドによって中断されることはありません。 Javaは、少なくともいくつかのアトミック操作を定義します。特に、longまたはdoubleアトミック以外の任意のタイプの変数への割り当て。割り当ての途中でスレッドがメソッドをプリエンプトすることを心配する必要はありません。実際には、この手段は、あなたは何もしない方法を同期するが、値を返す必要がない(または値に割り当てる)決してことbooleanintインスタンス変数を。同様に、ローカル変数と引数のみを使用して多くの計算を実行し、その計算の結果を最後に実行したものとしてインスタンス変数に割り当てるメソッドは、同期する必要はありません。例えば:

class some_class {int some_field; void f(some_class arg)//意図的に同期されていない{//ローカル変数とメソッド引数を使用する//ここで多くのことを行うが、//クラスのフィールドにはアクセスしない(または//にアクセスするメソッドを呼び出す)クラスのフィールド)。// ... some_field = new_value; //これを最後に行います。}}

一方、x=++yまたはを実行するとx+=y、インクリメント後、割り当ての前にプリエンプションされる可能性があります。この状況で原子性を取得するには、キーワードを使用する必要がありますsynchronized

同期のオーバーヘッドは重要であり、OSごとに異なる可能性があるため、これはすべて重要です。次のプログラムは、問題を示しています。各ループは、同じ操作を実行するメソッドを繰り返し呼び出しますが、メソッドの1つ(locking())は同期され、もう1つ()は同期されnot_locking()ません。プログラムは、Windows NT 4で実行されているJDK「パフォーマンスパック」VMを使用して、2つのループ間のランタイムの1.2秒の違い、つまり呼び出しごとに約1.2マイクロ秒を報告します。この違いはそれほど大きくはないように思われるかもしれませんが、通話時間は7.25パーセント増加しています。もちろん、メソッドがより多くの作業を行うにつれて、パーセンテージの増加は減少しますが、かなりの数のメソッド(少なくとも私のプログラムでは)はほんの数行のコードです。

インポートjava.util。*; class synch { synchronized int locking(int a、int b){return a + b;} int not_locking(int a、int b){return a + b;} private static final int ITERATIONS = 1000000; static public void main(String [] args){synch tester = new synch();ダブルスタート=新しいDate()。getTime(); for(long i = ITERATIONS; --i> = 0;)tester.locking(0,0);ダブルエンド=新しいDate()。getTime(); double lock_time = end-開始; start = new Date()。getTime(); for(long i = ITERATIONS; --i> = 0;)tester.not_locking(0,0);end = new Date()。getTime(); double not_locking_time = end-開始; double time_in_synchronization = lock_time-not_locking_time; System.out.println( "同期に失われた時間(ミリ秒):" + time_in_synchronization); System.out.println( "呼び出しごとのロックオーバーヘッド:" +(time_in_synchronization / ITERATIONS)); System.out.println(not_locking_time / locking_time * 100.0 + "%増加"); }}

HotSpot VMは同期オーバーヘッドの問題に対処することになっていますが、HotSpotはフリービーではありません。購入する必要があります。アプリでHotSpotのライセンスを取得して出荷しない限り、ターゲットプラットフォーム上にどのVMが配置されるかはわかりません。もちろん、プログラムの実行速度を、それを実行しているVMに依存させないようにする必要があります。デッドロックの問題(このシリーズの次回の記事で説明します)が存在しなかったとしても、「すべてを同期する」必要があるという考えはまったく間違っています。

並行性と並列性

次のOS関連の問題(およびプラットフォームに依存しないJavaの作成に関する主な問題)は、並行並列の概念に関係ています。並行マルチスレッドシステムでは、複数のタスクが同時に実行されているように見えますが、これらのタスクは実際にはチャンクに分割され、他のタスクのチャンクとプロセッサを共有します。次の図は、問題を示しています。並列システムでは、2つのタスクが実際に同時に実行されます。並列処理にはマルチCPUシステムが必要です。

ブロックされ、I / O操作が完了するのを待つことに多くの時間を費やしていない限り、複数の同時スレッドを使用するプログラムは、同等のシングルスレッドプログラムよりも実行速度が遅くなることがよくありますが、同等のシングルスレッドプログラムよりも整理されていることがよくあります。 -スレッドバージョン。複数のプロセッサで並行して実行されている複数のスレッドを使用するプログラムは、はるかに高速に実行されます。

Javaではスレッドを完全にVMに実装できますが、少なくとも理論的には、このアプローチではアプリケーションの並列処理が妨げられます。オペレーティングシステムレベルのスレッドが使用されていない場合、OSはVMインスタンスをシングルスレッドアプリケーションと見なします。これは、おそらくシングルプロセッサにスケジュールされます。最終的な結果として、複数のCPUがあり、VMが唯一のアクティブなプロセスであったとしても、同じVMインスタンスで実行されている2つのJavaスレッドが並行して実行されることはありません。もちろん、別々のアプリケーションを実行しているVMの2つのインスタンスを並行して実行することもできますが、それよりもうまくやりたいと思っています。並列処理を行うには、VMJavaスレッドをOSスレッドにマップします。したがって、プラットフォームの独立性が重要な場合は、さまざまなスレッドモデル間の違いを無視するわけにはいきません。

優先順位をまっすぐにする

SolarisとWindowsNTの2つのオペレーティングシステムを比較することにより、今説明した問題がプログラムにどのように影響するかを示します。

Javaは、少なくとも理論的には、スレッドに10の優先度レベルを提供します。(2つ以上のスレッドが両方とも実行を待機している場合、最も高い優先度レベルのスレッドが実行されます。)231の優先度レベルをサポートするSolarisでは、これは問題ありません(ただし、Solarisの優先度は使用が難しい場合があります。すぐに)。一方、NTには7つの優先度レベルがあり、これらはJavaの10にマッピングする必要があります。このマッピングは未定義であるため、多くの可能性があります。(たとえば、Java優先度レベル1と2は両方ともNT優先度レベル1にマップされ、Java優先度レベル8、9、および10はすべてNTレベル7にマップされる場合があります。)

優先度を使用してスケジューリングを制御する場合、NTの優先度レベルの不足が問題になります。優先度が固定されていないため、事態はさらに複雑になります。 NTは、優先度ブーストと呼ばれるメカニズムを提供します。これは、Cシステムコールでオフにできますが、Javaからはオフにできません。優先度のブーストが有効になっている場合、NTは、特定のI / O関連のシステムコールを実行するたびに、スレッドの優先度を不確定な時間だけ不確定な時間だけブーストします。実際には、これは、スレッドが厄介な時間にI / O操作を実行したため、スレッドの優先度が思ったよりも高くなる可能性があることを意味します。

優先度の向上のポイントは、バックグラウンド処理を実行しているスレッドがUIを多用するタスクの見かけの応答性に影響を与えないようにすることです。他のオペレーティングシステムには、通常、バックグラウンドプロセスの優先度を下げる、より洗練されたアルゴリズムがあります。このスキームの欠点は、特にプロセスレベルではなくスレッドごとに実装された場合、特定のスレッドがいつ実行されるかを決定するために優先度を使用することが非常に難しいことです。

悪化する。

Solarisでは、すべてのUnixシステムの場合と同様に、プロセスにはスレッドだけでなく優先順位もあります。優先度の高いプロセスのスレッドは、優先度の低いプロセスのスレッドによって中断されることはありません。さらに、特定のプロセスの優先度レベルをシステム管理者が制限して、ユーザープロセスが重要なOSプロセスを中断しないようにすることができます。 NTはこれをサポートしていません。 NTプロセスは単なるアドレス空間です。それ自体は優先順位がなく、スケジュールされていません。システムはスレッドをスケジュールします。次に、特定のスレッドがメモリ内にないプロセスで実行されている場合、プロセスはスワップインされます。NTスレッドの優先順位は、実際の優先順位の連続に分散されるさまざまな「優先順位クラス」に分類されます。システムは次のようになります。

The columns are actual priority levels, only 22 of which must be shared by all applications. (The others are used by NT itself.) The rows are priority classes. The threads running in a process pegged at the idle priority class are running at levels 1 through 6 and 15, depending on their assigned logical priority level. The threads of a process pegged as normal priority class will run at levels 1, 6 through 10, or 15 if the process doesn't have the input focus. If it does have the input focus, the threads run at levels 1, 7 through 11, or 15. This means that a high-priority thread of an idle priority class process can preempt a low-priority thread of a normal priority class process, but only if that process is running in the background. Notice that a process running in the "high" priority class only has six priority levels available to it. The other classes have seven.

NT provides no way to limit the priority class of a process. Any thread on any process on the machine can take over control of the box at any time by boosting its own priority class; there is no defense against this.

The technical term I use to describe NT's priority is unholy mess. In practice, priority is virtually worthless under NT.

So what's a programmer to do? Between NT's limited number of priority levels and it's uncontrollable priority boosting, there's no absolutely safe way for a Java program to use priority levels for scheduling. One workable compromise is to restrict yourself to Thread.MAX_PRIORITY, Thread.MIN_PRIORITY, and Thread.NORM_PRIORITY when you call setPriority(). This restriction at least avoids the 10-levels-mapped-to-7-levels problem. I suppose you could use the os.name system property to detect NT, and then call a native method to turn off priority boosting, but that won't work if your app is running under Internet Explorer unless you also use Sun's VM plug-in. (Microsoft's VM uses a nonstandard native-method implementation.) In any event, I hate to use native methods. I usually avoid the problem as much as possible by putting most threads at NORM_PRIORITY and using scheduling mechanisms other than priority. (I'll discuss some of these in future installments of this series.)

Cooperate!

There are typically two threading models supported by operating systems: cooperative and preemptive.

The cooperative multithreading model

協力、それは(決してしない可能性がある)それを放棄することを決定するまで、システム、スレッドは、そのプロセッサの制御を保持します。さまざまなスレッドが相互に連携する必要があります。そうしないと、1つを除くすべてのスレッドが「不足」します(つまり、実行する機会が与えられません)。ほとんどの協調システムでのスケジューリングは、優先度レベルによって厳密に行われます。現在のスレッドが制御を放棄すると、最も優先度の高い待機中のスレッドが制御を取得します。(このルールの例外はWindows 3.xです。これは協調モデルを使用しますが、スケジューラーはあまりありません。フォーカスのあるウィンドウが制御を取得します。)