Java 101:Javaスレッドを理解する、パート2:スレッドの同期

先月、スレッドオブジェクトの作成、Threadstart()メソッドを呼び出すことでそれらのオブジェクトに関連付けられたスレッドを開始すること、およびThread3つのオーバーロードされたメソッドなどの他のメソッドを呼び出すことで簡単なスレッド操作を実行することがいかに簡単かを示しましたjoin()。今月は、より複雑なマルチスレッドJavaプログラムを取り上げます。

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

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

マルチスレッドプログラムは、スレッドの同期がないために、機能が不安定になったり、誤った値を生成したりすることがよくあります。同期とは、複数のスレッドがクラスとインスタンスのフィールド変数、およびその他の共有リソースを操作できるようにするコードシーケンスへのスレッドアクセスをシリアル化(または一度に1つずつ順序付け)する行為です。私はそれらのコードシーケンスを重要なコードセクションと呼んでいます。。今月のコラムでは、同期を使用して、プログラムの重要なコードセクションへのスレッドアクセスをシリアル化します。

一部のマルチスレッドプログラムが同期を使用する必要がある理由を示す例から始めます。次に、モニターとロック、およびsynchronizedキーワードの観点からJavaの同期メカニズムについて説明します。同期メカニズムを誤って使用するとその利点が失われるため、このような誤用に起因する2つの問題を調査して結論を​​出します。

ヒント:クラスおよびインスタンスフィールド変数とは異なり、スレッドはローカル変数およびパラメーターを共有できません。理由:ローカル変数とパラメーターは、スレッドのメソッド呼び出しスタックに割り当てられます。その結果、各スレッドはそれらの変数の独自のコピーを受け取ります。対照的に、これらの変数はスレッドのメソッド呼び出しスタックに割り当てられないため、スレッドはクラスフィールドとインスタンスフィールドを共有できます。代わりに、クラス(クラスフィールド)またはオブジェクト(インスタンスフィールド)の一部として、共有ヒープメモリに割り当てます。

同期の必要性

なぜ同期が必要なのですか?答えについては、次の例を検討してください。金融取引の引き出し/預金をシミュレートするためにスレッドのペアを使用するJavaプログラムを作成します。そのプログラムでは、一方のスレッドが入金を実行し、もう一方のスレッドが引き出しを実行します。各スレッドは、金融取引の名前と金額を識別する共有変数のペアクラス変数とインスタンスフィールド変数を操作します。正しい金融トランザクションの場合、他のスレッドがとに値を割り当て始める(およびそれらの値を印刷する)前に、各スレッドは変数nameamount変数への値の割り当てを完了する必要があります(トランザクションの保存をシミュレートするためにそれらの値を出力します)。いくつかの作業の後、リスト1に似たソースコードが作成されます。nameamount

リスト1.NeedForSynchronizationDemo.java

// NeedForSynchronizationDemo.java class NeedForSynchronizationDemo { public static void main (String [] args) { FinTrans ft = new FinTrans (); TransThread tt1 = new TransThread (ft, "Deposit Thread"); TransThread tt2 = new TransThread (ft, "Withdrawal Thread"); tt1.start (); tt2.start (); } } class FinTrans { public static String transName; public static double amount; } class TransThread extends Thread { private FinTrans ft; TransThread (FinTrans ft, String name) { super (name); // Save thread's name this.ft = ft; // Save reference to financial transaction object } public void run () { for (int i = 0; i < 100; i++) { if (getName ().equals ("Deposit Thread")) { // Start of deposit thread's critical code section ft.transName = "Deposit"; try { Thread.sleep ((int) (Math.random () * 1000)); } catch (InterruptedException e) { } ft.amount = 2000.0; System.out.println (ft.transName + " " + ft.amount); // End of deposit thread's critical code section } else { // Start of withdrawal thread's critical code section ft.transName = "Withdrawal"; try { Thread.sleep ((int) (Math.random () * 1000)); } catch (InterruptedException e) { } ft.amount = 250.0; System.out.println (ft.transName + " " + ft.amount); // End of withdrawal thread's critical code section } } } }

NeedForSynchronizationDemoのソースコードには、2つの重要なコードセクションがあります。1つは入金スレッドにアクセスでき、もう1つは引き出しスレッドにアクセスできます。デポジットスレッドの重要なコードセクション内で、そのスレッドはDepositStringオブジェクトの参照を共有変数transNameに割り当て2000.0、共有変数に割り当てますamount。同様に、取り出し糸のクリティカルコードセクション内に、そのスレッドが割り当てWithdrawalStringへのオブジェクトの参照transNameおよび譲受人250.0にしますamount。各スレッドの割り当てに続いて、それらの変数の内容が出力されます。を実行するとNeedForSynchronizationDemo、散在する行のリストと同様の出力が期待される場合がWithdrawal 250.0ありDeposit 2000.0ます。代わりに、次のような出力を受け取ります。

Withdrawal 250.0 Withdrawal 2000.0 Deposit 2000.0 Deposit 2000.0 Deposit 250.0

プログラムには間違いなく問題があります。引き出しスレッドは$ 2000の引き出しをシミュレートしてはならず、預金スレッドは$ 250の預金をシミュレートしてはなりません。各スレッドは一貫性のない出力を生成します。これらの不一致の原因は何ですか?次のことを考慮してください。

  • シングルプロセッサマシンでは、スレッドがプロセッサを共有します。その結果、1つのスレッドは特定の期間しか実行できません。そのとき、JVM /オペレーティングシステムはそのスレッドの実行を一時停止し、別のスレッドの実行を許可します。これは、スレッドスケジューリングの兆候であり、パート3で説明するトピックです。マルチプロセッサマシンでは、スレッドとプロセッサの数に応じて、各スレッド独自のプロセッサを持つことができます。
  • シングルプロセッサマシンでは、スレッドの実行期間は、別のスレッドが独自のクリティカルコードセクションの実行を開始する前に、そのスレッドがクリティカルコードセクションの実行を終了するのに十分な長さではない場合があります。マルチプロセッサマシンでは、スレッドは重要なコードセクションでコードを同時に実行できます。ただし、重要なコードセクションに異なる時間に入力する場合があります。
  • シングルプロセッサマシンまたはマルチプロセッサマシンのいずれかで、次のシナリオが発生する可能性があります。スレッドAは、重要なコードセクションの共有変数Xに値を割り当て、100ミリ秒を必要とする入出力操作を実行することを決定します。次に、スレッドBは重要なコードセクションに入り、Xに異なる値を割り当て、50ミリ秒の入出力操作を実行し、共有変数YとZに値を割り当てます。スレッドAの入出力操作が完了し、そのスレッドは独自の値を割り当てます。 XにはBが割り当てられた値が含まれているのに対し、YとZにはAが割り当てられた値が含まれているため、不整合が発生します。

不整合はどのように発生しNeedForSynchronizationDemoますか?デポジットスレッドが実行されてft.transName = "Deposit";からを呼び出すとしますThread.sleep()。その時点で、デポジットスレッドは、プロセッサがスリープ状態でなければならない期間、プロセッサの制御を放棄し、引き出しスレッドが実行されます。デポジットスレッドが500ミリ秒スリープすると仮定します(Math.random()0から999ミリ秒までの範囲でランダムに選択された値。今後の記事でMathそのrandom()方法について説明します)。デポジットスレッドのスリープ時間中、引き出しスレッドは実行されft.transName = "Withdrawal";、50ミリ秒(引き出しスレッドのランダムに選択されたスリープ値)スリープし、スリープ解除、実行ft.amount = 250.0;、実行されSystem.out.println (ft.transName + " " + ft.amount);ます。これらはすべて、デポジットスレッドがスリープ解除される前に行われます。その結果、引き出しスレッドが印刷されますWithdrawal 250.0、 どちらが正しい。デポジットスレッドが起動するとft.amount = 2000.0;、が実行され、続いてSystem.out.println (ft.transName + " " + ft.amount);。が実行されます。今回はWithdrawal 2000.0印刷しますが、これは正しくありません。デポジットスレッドは以前に"Deposit"の参照をtransNameに割り当てましたが、引き出しスレッド"Withdrawal"がその共有変数にの参照を割り当てたときに、その参照はその後消えました。デポジットスレッドが起動すると、への正しい参照を復元できませんでしたが、にtransName割り当て2000.0て実行を継続しましたamount。どちらの変数にも無効な値はありませんが、両方の変数を組み合わせた値は不整合を表しています。この場合、それらの値は、000を引き出す試みを表します。

ずっと前に、コンピューターサイエンティストは、不整合につながる複数のスレッドの複合動作を説明する用語を発明しました。その用語は競合状態です。つまり、他のスレッドが同じクリティカルコードセクションに入る前に、各スレッドがそのクリティカルコードセクションを完了するために競合する行為です。なのでNeedForSynchronizationDemo示すように、スレッドの実行順序は予測できません。他のスレッドがそのセクションに入る前に、スレッドがその重要なコードセクションを完了することができるという保証はありません。したがって、競合状態が発生し、不整合が発生します。競合状態を防ぐために、各スレッドは、別のスレッドが同じクリティカルコードセクションまたは同じ共有変数またはリソースを操作する別の関連するクリティカルコードセクションに入る前に、クリティカルコードセクションを完了する必要があります。重要なコードセクションへのアクセスをシリアル化する手段、つまり一度に1つのスレッドのみへのアクセスを許可する手段がなければ、競合状態や不整合を防ぐことはできません。幸い、Javaは、同期メカニズムを介してスレッドアクセスをシリアル化する方法を提供します。

:Javaのタイプのうち、長整数および倍精度浮動小数点変数のみが不整合になりがちです。どうして?32ビットJVMは通常、2つの隣接する32ビットステップで64ビット長整数変数または64ビット倍精度浮動小数点変数にアクセスします。1つのスレッドが最初のステップを完了してから、別のスレッドが両方のステップを実行するまで待機する場合があります。次に、最初のスレッドが起動して2番目のステップを完了し、最初または2番目のスレッドの値とは異なる値を持つ変数を生成する場合があります。その結果、少なくとも1つのスレッドが長整数変数または倍精度浮動小数点変数のいずれかを変更できる場合、その変数を読み取ったり変更したりするすべてのスレッドは、同期を使用して変数へのアクセスをシリアル化する必要があります。

Javaの同期メカニズム

Javaは、複数のスレッドが任意の時点で1つ以上の重要なコードセクションでコードを実行するのを防ぐための同期メカニズムを提供します。このメカニズムは、モニターとロックの概念に基づいています。モニターは、重要なコードセクションとロックの保護ラッパーと考えてください。複数のスレッドがモニターに入るのを防ぐためにモニターが使用するソフトウェアエンティティとして。アイデアは次のとおりです。スレッドがモニターで保護された重要なコードセクションに入ろうとする場合、そのスレッドは、モニターに関連付けられているオブジェクトに関連付けられているロックを取得する必要があります。 (各オブジェクトには独自のロックがあります。)他のスレッドがそのロックを保持している場合、JVMは要求元のスレッドをモニター/ロックに関連付けられた待機領域で待機させます。モニター内のスレッドがロックを解放すると、JVMは待機中のスレッドをモニターの待機領域から削除し、そのスレッドがロックを取得してモニターの重要なコードセクションに進むことを許可します。

モニター/ロックを操作するために、JVMはmonitorenterおよびmonitorexit命令を提供します。幸いなことに、このような低レベルで作業する必要はありません。代わりに、ステートメントおよび同期メソッドのsynchronizedコンテキストでJavaのキーワードを使用できますsynchronized

同期されたステートメント

一部の重要なコードセクションは、それらを囲むメソッドのごく一部を占めています。このような重要なコードセクションへの複数のスレッドアクセスを保護するには、synchronizedステートメントを使用します。そのステートメントの構文は次のとおりです。

'synchronized' '(' objectidentifier ')' '{' // Critical code section '}'

synchronized文はキーワードで始まりsynchronizedとして続けてobjectidentifier、丸括弧のペアの間に表示されます。objectidentifierの参照を持つロックすることをモニターに関連付けるオブジェクトsynchronizedの文が表します。最後に、Javaステートメントの重要なコードセクションが中括弧文字のペアの間に表示されます。synchronizedステートメントをどのように解釈しますか?次のコードフラグメントについて考えてみます。

synchronized ("sync object") { // Access shared variables and other shared resources }