スレッド通信の目標は、スレッドが互いに信号を送信できるようにすることです。一方、スレッド通信により、スレッドは他のスレッドからの信号を待つことができます。
共有オブジェクトを介した通信
スレッド間で信号を送信する簡単な方法は、共有オブジェクトの変数に信号値を設定することです。スレッドAは、ブールメンバー変数のhasDatatoProcessを同期ブロックでtrueに設定し、スレッドBは同期ブロックのHasdatatoprocessメンバー変数も読み取ります。この単純な例は、信号を保持しているオブジェクトを使用して、セットとチェック方法を提供します。
パブリッククラスmysignal {保護されたboolean hasdatatoprocess = false; public同期ブールンhasdatatoprocess(){return this.hasdatatoprocess; } public Synchronized void sethasdatatoprocess(boolean hasdata){this.hasdatatoprocess = hasdata; }}スレッドAとBは、通信のためにMySignal共有インスタンスへの参照を取得する必要があります。参照が異なるマイシンガルインスタンスを指している場合、お互いの信号を検出することはできません。処理されるデータは、MySignalインスタンスとは別に保存される共有キャッシュ領域に保存できます。
忙しい待って
データを処理する準備をしているスレッドBは、データが利用可能になるのを待っています。言い換えれば、スレッドAからの信号が待機しているため、hasdatatoprocess()がtrueを返します。スレッドBはループで実行され、この信号を待ちます。
保護されたmysignal sharedsignal = ...... while(!sharedsignal.hasdatatoprocess()){//何もしない...wait()、notify()、notifyall()
忙しい待機は、平均待機時間が非常に短い場合を除き、待機スレッドを実行しているCPUを効果的に利用しません。そうでなければ、待っている信号を受け取るまで、待っている糸を眠りさせるか、非暴走を眠らせることが賢明です。
Javaには、信号を待っている間にスレッドが走り回ることができるようにするための組み込みの待機メカニズムがあります。 java.lang.objectクラスは、この待機メカニズムを実装するために、3つの方法を定義します。
スレッドが任意のオブジェクトのwait()メソッドを呼び出すと、別のスレッドが同じオブジェクトのnotify()メソッドを呼び出すまで、非ランニング状態になります。 wait()またはnotify()を呼び出すには、最初にそのオブジェクトのロックを取得する必要があります。つまり、スレッドは同期ブロックでwait()またはnotify()を呼び出す必要があります。以下は、wait()とnotify()を使用して、mysingalの変更されたバージョンです。
public class monitorobject {} public class mywaitnotify {monitorobject mymonitorobject = new MonitorObject(); public void dowait(){synchronized(mymonitorobject){try {mymonitorobject.wait(); } catch(arternedexception e){...}}} public void donotify(){synchronized(mymonitorobject){mymonitorobject.notify(); }}}待機スレッドはdowait()を呼び出し、ウェイクアップスレッドはdonotify()を呼び出します。スレッドがオブジェクトのnotify()メソッドを呼び出すと、オブジェクトを待機するスレッドの1つが目覚めて実行されます(注:このスレッドはランダムであり、目覚めるスレッドを指定できません)。また、特定のオブジェクトを待っているすべてのスレッドを目覚めさせるためのnotifyall()メソッドも提供されています。
ご覧のとおり、それが待機スレッドであろうとウェイクアップスレッドであろうと、同期ブロックでwait()とnotify()を呼び出します。これは必須です!スレッドがオブジェクトロックを保持していない場合、wait()、notify()、またはnotifyall()を呼び出すことはできません。それ以外の場合、IllegalMonitorStateExceptionの例外がスローされます。
(注:これはJVMの実装方法です。待機を呼び出すと、最初に現在のスレッドがロックの所有者であるかどうかを確認し、IllegalMonitorStateExceptをスローします。)
しかし、これはどのように可能ですか?スレッドが同期ブロックで実行されるのを待つとき、モニターオブジェクト(MyMonitorオブジェクト)のロックを常に保持していませんか?待機スレッドは、donotify()の同期ブロックに入るウェイクアップスレッドをブロックできますか?答えは次のとおりです。それは本当です。スレッドがwait()メソッドを呼び出すと、保持されているモニターオブジェクトのロックが解放されます。これにより、他のスレッドがwait()またはnotify()を呼び出すことができます。
スレッドが目覚めたら、wait()メソッド呼び出しは、notify()が呼び出されるまですぐに終了できません。
public class mywaitnotify2 {monitorobject mymonitorobject = new MonitorObject(); boolean wassignaled = false; public void dowait(){synchronized(mymonitorobject){if(!wassignaled){try {mymonitorobject.wait(); } catch(arternedexception e){...}} //クリア信号をクリアし、実行を続行します。 wassignaled = false; }} public void donotify(){synchronized(mymonitorobject){wassignaled = true; mymonitorobject.notify(); }}}
スレッドは独自の同期ブロックを終了します。言い換えれば、Wait Method Callが同期ブロックで実行されるため、Wait()メソッド呼び出しを終了する前に、Wakenスレッドはモニターオブジェクトのロックを回復する必要があります。 notifyall()によって複数のスレッドが目覚めた場合、同時に1つのスレッドのみがwait()メソッドを終了できます。各スレッドは、wait()を終了する前にモニターオブジェクトのロックを取得する必要があるためです。
逃した信号
notify()およびnotifyall()メソッドは、それらを呼び出すメソッドを保存しません。これらの2つのメソッドが呼び出された場合、スレッドが待機状態にない可能性があるためです。通知信号は破棄されました。したがって、wait()を呼び出す前に通知される前にスレッドがnotify()を呼び出す場合、待機スレッドはこの信号を逃します。これは問題である場合とそうでない場合があります。ただし、場合によっては、これにより、待機スレッドが常に待機し、スレッドがウェイクアップ信号を逃すため、目覚めなくなる可能性があります。
信号を失わないようにするには、信号クラスで保存する必要があります。 MyWaitNotifyの例では、通知信号はMyWaitNotifyインスタンスのメンバー変数に保存する必要があります。これは、mywaitnotifyの変更されたバージョンです。
public class mywaitnotify2 {monitorobject mymonitorobject = new MonitorObject(); boolean wassignaled = false; public void dowait(){synchronized(mymonitorobject){if(!wassignaled){try {mymonitorobject.wait(); } catch(arternedexception e){...}} //クリア信号をクリアし、実行を続行します。 wassignaled = false; }} public void donotify(){synchronized(mymonitorobject){wassignaled = true; mymonitorobject.notify(); }}}donotify()メソッドは、notify()を呼び出す前に、wassignalled変数をtrueに設定することに注意してください。同時に、dowait()メソッドは、wait()を呼び出す前にwassignaled変数をチェックすることに注意してください。実際、以前のdowait()コールとこのdowait()呼び出しまでの期間中に信号が受信されない場合、wait()のみを呼び出します。
(証明書:信号損失を避けるために、変数を使用して通知されたかどうかを保存します。通知する前に、通知されたように設定します。待った後、通知がないように設定し、通知を待つ必要があります。)
偽の目覚め
何らかの理由で、notify()とnotifyall()を呼び出すことなく、スレッドが目覚める可能性があります。これはスプリアスウェイクアップと呼ばれます。理由もなく目を覚ます。
MywaitNotify2のdowait()方法で誤ったウェイクアップが発生した場合、待機スレッドは、正しい信号を受け取らない場合でも、後続の操作を実行できます。これにより、アプリケーションに深刻な問題が発生する可能性があります。
誤検知を防ぐために、信号を保持するメンバー変数は、IF式ではなく、時間ループでチェックされます。このような時間ループはスピンロックと呼ばれます(注:このアプローチは注意する必要があります。現在のJVM実装スピンはCPUを消費します。DONOTIFYメソッドが長期間呼び出されない場合、DOWAITメソッドは継続的に回転し、CPUは消費しすぎます)。目覚めたスレッドは、スピンロックの条件(ループ)が偽になるまで回転します。 MyWaitNotify2の次の変更バージョンはこれを示しています。
public class mywaitnotify3 {monitorobject mymonitorobject = new MonitorObject(); boolean wassignaled = false; public void dowait(){synchronized(mymonitorobject){while(!wassignaled){try {mymonitorobject.wait(); } catch(arternedexception e){...}} //クリア信号をクリアし、実行を続行します。 wassignaled = false; }} public void donotify(){synchronized(mymonitorobject){wassignaled = true; mymonitorobject.notify(); }}}wait()メソッドは、if式ではなく、whileループにあることに注意してください。信号を受信せずに待っているスレッドが目覚めた場合、Wassignaled変数は偽りになり、Whileループが再び実行され、ウェイクアップスレッドが待機状態に戻るように促します。
複数のスレッドが同じ信号を待っています
複数のスレッドが待機していて、notifyall()によって目覚めている場合は、1つだけが実行を続けることが許可されている場合、しばらくループを使用することも良い方法です。毎回モニターオブジェクトのロックを取得できるスレッドは1つだけです。つまり、1つのスレッドのみがwait()呼び出しを終了して、wassignaledフラグをクリアできます(falseに設定)。このスレッドがdowait()同期ブロックを終了すると、他のスレッドはwait()呼び出しを終了し、whileループでwassignaled変数値を確認します。ただし、このフラグは最初の目覚めたスレッドによってクリアされているため、残りの目覚めたスレッドは、次に信号が到着するまで待機状態に戻ります。
文字列定数またはグローバルオブジェクトでwait()を呼び出さないでください
(証明注:この章に記載されている文字列定数は、一定の値の変数を参照しています)
この記事の以前のバージョンでは、MyWaitNotifyの例で文字列定数( "")をパイプオブジェクトとして使用します。これが例です:
public class mywaitnotify {string mymonitorobject = ""; boolean wassignaled = false; public void dowait(){synchronized(mymonitorobject){while(!wassignaled){try {mymonitorobject.wait(); } catch(arternedexception e){...}} //クリア信号をクリアし、実行を続行します。 wassignaled = false; }} public void donotify(){synchronized(mymonitorobject){wassignaled = true; mymonitorobject.notify(); }}}空の文字列の同期ブロックでlock(または他の定数文字列)としてwait()とnotify()を呼び出して引き起こされる問題は、JVM/コンパイラが定数文字列を同じオブジェクトに変換することです。これは、2つの異なるMyWaitNotifyインスタンスがある場合でも、それらはすべて同じ空の文字列インスタンスを参照することを意味します。また、最初のmywaitNotifyインスタンスでdowait()を呼び出すスレッドが、2番目のmywaitnotifyインスタンスでdonotify()を呼び出すスレッドによって目覚められるリスクがあることを意味します。この状況は次のように描画できます。
最初はこれは大きな問題ではないかもしれません。結局のところ、2番目のmywaitnotifyインスタンスでdonotify()が呼び出された場合、実際に起こるのは、スレッドAとBが誤って目覚めていることです。ウェイクアップスレッド(aまたはb)は、whileループで信号値をチェックしてから待機状態に戻ります。これは、donotify()が最初のmywaitnotifyインスタンスで呼び出されないため、これが待っているインスタンスです。この状況は、誤った目覚めをトリガーすることと同等です。スレッドAまたはBは、信号値を更新せずに目覚めます。しかし、コードはこの状況を処理するため、スレッドは待機状態に戻ります。 4つのスレッドが同じ共有文字列インスタンスでwait()およびnotify()を呼び出しても、dowait()とdonotify()の信号は、それぞれ2つのmywaitnotifyインスタンスによって保存されます。 mywaitnotify1のdonotify()の呼び出しは、mywaitnotify2のスレッドを目覚める可能性がありますが、信号値はmywaitnotify1でのみ保存されます。
問題は、donotify()がnotifyall()の代わりにnotify()のみを呼び出すため、4つのスレッドが同じ文字列(空)インスタンスを待っている場合でも、1つのスレッドのみが目覚めていることです。したがって、cまたはdに送信された信号によってスレッドAまたはbが目覚めた場合、独自の信号値をチェックして、信号が受信されるかどうかを確認し、その後待機状態に戻ります。 CもDも目覚めて、実際に受け取った信号値を確認したため、信号は失われました。この状況は、上記の信号が欠落している問題に相当します。 CとDは信号に送信されましたが、信号にも応答することはできません。
donotify()メソッドがnotify()の代わりにnotifyall()を呼び出す場合、すべての待機スレッドが目覚め、信号値が順番にチェックされます。スレッドAとBは待機状態に戻りますが、CまたはDの1つのスレッドのみが信号に気付き、DoWait()メソッドコールを終了します。 CまたはDのもう1つは、信号を取得したスレッドがDoWait()を終了するプロセス中に信号値(FALSEに設定)をクリアするため、待機状態に戻ります。
上記の段落を読んだ後、notify()の代わりにnotifyall()を使用しようとするかもしれませんが、これはパフォーマンスベースの悪いアイデアです。信号に1つのスレッドのみが応答できる場合、毎回すべてのスレッドを目覚める理由はありません。
したがって、wait()/notify()メカニズムでは、グローバルオブジェクト、文字列定数などを使用しないでください。対応する一意のオブジェクトを使用する必要があります。たとえば、mywaitnotify3の各インスタンスには、空の文字列にwait()/notify()を呼び出す代わりに、独自のモニターオブジェクトがあります。
上記は、Javaマルチスレッドとスレッド通信に関する情報です。今後も関連情報を追加し続けます。このサイトへのご支援ありがとうございます!