スレッド通信の目標は、スレッドが相互にシグナルを送信できるようにすることです。一方、スレッド通信により、スレッドは他のスレッドからのシグナルを待つことができます。
共有オブジェクトを介した通信
待機中のビジー
wait()、notify()、およびnotifyAll()
シグナルの欠落
誤ったウェイクアップ
同じシグナルを待機している複数のスレッド
定数文字列またはグローバル オブジェクト wait()
共有オブジェクトを介した通信
スレッド間でシグナルを送信する簡単な方法は、共有オブジェクトの変数にシグナル値を設定することです。スレッド A は同期ブロック内のブール型メンバー変数 hasDataToProcess を true に設定し、スレッド B も同期ブロック内のメンバー変数 hasDataToProcess を読み取ります。この簡単な例では、シグナルを保持し、set メソッドと check メソッドを提供するオブジェクトを使用します。
public class MySignal{ protected boolean hasDataToProcess = false; public synchronized boolean hasDataToProcess(){ return this.hasDataToProcess; } public synchronized void setHasDataToProcess(boolean hasData){ this.hasDataToProcess = hasData; } }
スレッド A と B は、通信するために MySignal の共有インスタンスへの参照を取得する必要があります。それらが異なる MySingal インスタンスへの参照を保持している場合、お互いは相手のシグナルを検出できなくなります。処理する必要があるデータは、MySignal インスタンスとは別に保存される共有キャッシュに保存できます。
ビジー待機
データの処理を準備しているスレッド B は、データが使用可能になるのを待っています。つまり、hasDataToProcess() が true を返すスレッド A からのシグナルを待っています。スレッド B は、このシグナルを待機するループ内で実行されます:
protected MySignal sharedSignal = ... ... while(!sharedSignal.hasDataToProcess()){ //do nothing... busy waiting }
wait()、notify()、およびnotifyAll()
平均待機時間が非常に短い場合を除き、ビジー待機では待機スレッドを実行している CPU が効率的に使用されません。 。それ以外の場合は、待機中のスレッドをスリープ状態にするか、待機中のシグナルを受信するまで非実行状態にしておく方が賢明です。
Java には、シグナルの待機中にスレッドを非実行状態にできる待機メカニズムが組み込まれています。 java.lang.Object クラスは、この待機メカニズムを実装するために、wait()、notify()、notifyAll() の 3 つのメソッドを定義します。
スレッドが任意のオブジェクトの wait() メソッドを呼び出すと、別のスレッドが同じオブジェクトの notify() メソッドを呼び出すまで、そのスレッドは非実行状態になります。 wait() または Notice() を呼び出すには、スレッドはまずそのオブジェクトのロックを取得する必要があります。つまり、スレッドは同期ブロック内で wait() または Notice() を呼び出す必要があります。これは、MySingal の修正バージョンです。wait() と Notice() を使用した MyWaitNotify です:
public class MonitorObject{ } public class MyWaitNotify{ MonitorObject myMonitorObject = new MonitorObject(); public void doWait(){ synchronized(myMonitorObject){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } } public void doNotify(){ synchronized(myMonitorObject){ myMonitorObject.notify(); } } }
待機中のスレッドは doWait() を呼び出し、起動中のスレッドは doNotify() を呼び出します。スレッドがオブジェクトのnotify()メソッドを呼び出すと、オブジェクトを待機しているすべてのスレッドのうちの1つのスレッドが起動され、実行が許可されます(注: 起動されるスレッドはランダムであり、どのスレッドを起動するかを指定することはできません)。 。また、指定されたオブジェクトを待機しているすべてのスレッドを起動するための NoticeAll() メソッドも提供します。
ご覧のとおり、待機中のスレッドと起動中のスレッドの両方が、同期ブロック内で wait() と Notice() を呼び出します。これは必須です!スレッドは、オブジェクト ロックを保持していない場合、wait()、notify()、またはnotifyAll()を呼び出すことができません。それ以外の場合は、IllegalMonitorStateException 例外がスローされます。
(注: これが JVM の実装方法です。wait を呼び出すと、まず現在のスレッドがロックの所有者であるかどうかがチェックされます。そうでない場合は、IllegalMonitorStateExcept がスローされます。)
しかし、これはどのようにして可能でしょうか?待機中のスレッドが同期ブロック内で実行される場合、常にモニターオブジェクト(myMonitorオブジェクト)のロックを保持しているのではありませんか?待機中のスレッドは、起動中のスレッドが doNotify() の同期ブロックに入るのをブロックできませんか?答えは「実際にはそうではありません」です。スレッドが wait() メソッドを呼び出すと、モニター オブジェクトに対して保持しているロックが解放されます。これにより、他のスレッドも wait() または Notice() を呼び出すことができるようになります。
スレッドが目覚めると、notify() を呼び出す
public class MyWaitNotify2{ MonitorObject myMonitorObject = new MonitorObject(); boolean wasSignalled = false; public void doWait(){ synchronized(myMonitorObject){ if(!wasSignalled){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } //clear signal and continue running. wasSignalled = false; } } public void doNotify(){ synchronized(myMonitorObject){ wasSignalled = true; myMonitorObject.notify(); } } } <br>
スレッドが独自の同期ブロックを終了するまで、wait() メソッド呼び出しをすぐに終了することはできません。言い換えると、wait メソッド呼び出しは同期ブロック内で実行されるため、ウェイクアップしたスレッドは wait() メソッド呼び出しを終了する前にモニター オブジェクトのロックを再取得する必要があります。複数のスレッドがnotifyAll()によって起動される場合、各スレッドはwait()を終了する前にモニター・オブジェクトのロックを取得する必要があるため、同時に1つのスレッドのみがwait()メソッドを終了できます。
欠落シグナル
notify() メソッドとnotifyAll() メソッドは、それらを呼び出すメソッドを保存しません。これは、これら 2 つのメソッドが呼び出されたときに、待機状態のスレッドが存在しない可能性があるためです。通知信号は有効期限が切れると破棄されます。したがって、通知されたスレッドが wait() を呼び出す前にスレッドが notify() を呼び出した場合、待機中のスレッドはシグナルを見逃すことになります。これは問題になる場合もあれば、問題にならない場合もあります。ただし、場合によっては、待機中のスレッドがウェイクアップ信号を見逃したため、待機中のスレッドが永久に待機し、ウェイクアップしない可能性があります。
信号の損失を避けるために、信号を signal クラスに保存する必要があります。 MyWaitNotify の例では、通知シグナルは MyWaitNotify インスタンスのメンバー変数に格納される必要があります。 MyWaitNotify の修正バージョンは次のとおりです:
public class MyWaitNotify2{ MonitorObject myMonitorObject = new MonitorObject(); boolean wasSignalled = false; public void doWait(){ synchronized(myMonitorObject){ if(!wasSignalled){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } //clear signal and continue running. wasSignalled = false; } } public void doNotify(){ synchronized(myMonitorObject){ wasSignalled = true; myMonitorObject.notify(); } } }
留意 doNotify()方法在调用 notify()前把 wasSignalled 变量设为 true。同时,留意 doWait()方法在调用 wait()前会检查 wasSignalled 变量。事实上,如果没有信号在前一次 doWait()调用和这次 doWait()调用之间的时间段里被接收到,它将只调用 wait()。
(校注:为了避免信号丢失, 用一个变量来保存是否被通知过。在 notify 前,设置自己已经被通知过。在 wait 后,设置自己没有被通知过,需要等待通知。)
假唤醒
由于莫名其妙的原因,线程有可能在没有调用过 notify()和 notifyAll()的情况下醒来。这就是所谓的假唤醒(spurious wakeups)。无端端地醒过来了。
如果在 MyWaitNotify2 的 doWait()方法里发生了假唤醒,等待线程即使没有收到正确的信号,也能够执行后续的操作。这可能导致你的应用程序出现严重问题。
为了防止假唤醒,保存信号的成员变量将在一个 while 循环里接受检查,而不是在 if 表达式里。这样的一个 while 循环叫做自旋锁(校注:这种做法要慎重,目前的 JVM 实现自旋会消耗 CPU,如果长时间不调用 doNotify 方法,doWait 方法会一直自旋,CPU 会消耗太大)。被唤醒的线程会自旋直到自旋锁(while 循环)里的条件变为 false。以下 MyWaitNotify2 的修改版本展示了这点:
public class MyWaitNotify3{ MonitorObject myMonitorObject = new MonitorObject(); boolean wasSignalled = false; public void doWait(){ synchronized(myMonitorObject){ while(!wasSignalled){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } //clear signal and continue running. wasSignalled = false; } } public void doNotify(){ synchronized(myMonitorObject){ wasSignalled = true; myMonitorObject.notify(); } } }
留意 wait()方法是在 while 循环里,而不在 if 表达式里。如果等待线程没有收到信号就唤醒,wasSignalled 变量将变为 false,while 循环会再执行一次,促使醒来的线程回到等待状态。
多个线程等待相同信号
如果你有多个线程在等待,被 notifyAll()唤醒,但只有一个被允许继续执行,使用 while 循环也是个好方法。每次只有一个线程可以获得监视器对象锁,意味着只有一个线程可以退出 wait()调用并清除 wasSignalled 标志(设为 false)。一旦这个线程退出 doWait()的同步块,其他线程退出 wait()调用,并在 while 循环里检查 wasSignalled 变量值。但是,这个标志已经被第一个唤醒的线程清除了,所以其余醒来的线程将回到等待状态,直到下次信号到来。
不要在字符串常量或全局对象中调用 wait()
(校注:本章说的字符串常量指的是值为常量的变量)
本文早期的一个版本在 MyWaitNotify 例子里使用字符串常量(””)作为管程对象。以下是那个例子:
public class MyWaitNotify{ String myMonitorObject = ""; boolean wasSignalled = false; public void doWait(){ synchronized(myMonitorObject){ while(!wasSignalled){ try{ myMonitorObject.wait(); } catch(InterruptedException e){...} } //clear signal and continue running. wasSignalled = false; } } public void doNotify(){ synchronized(myMonitorObject){ wasSignalled = true; myMonitorObject.notify(); } } }
在空字符串作为锁的同步块(或者其他常量字符串)里调用 wait()和 notify()产生的问题是,JVM/编译器内部会把常量字符串转换成同一个对象。这意味着,即使你有 2 个不同的 MyWaitNotify 实例,它们都引用了相同的空字符串实例。同时也意味着存在这样的风险:在第一个 MyWaitNotify 实例上调用 doWait()的线程会被在第二个 MyWaitNotify 实例上调用 doNotify()的线程唤醒。这种情况可以画成以下这张图:
起初这可能不像个大问题。毕竟,如果 doNotify()在第二个 MyWaitNotify 实例上被调用,真正发生的事不外乎线程 A 和 B 被错误的唤醒了 。这个被唤醒的线程(A 或者 B)将在 while 循环里检查信号值,然后回到等待状态,因为 doNotify()并没有在第一个 MyWaitNotify 实例上调用,而这个正是它要等待的实例。这种情况相当于引发了一次假唤醒。线程 A 或者 B 在信号值没有更新的情况下唤醒。但是代码处理了这种情况,所以线程回到了等待状态。记住,即使 4 个线程在相同的共享字符串实例上调用 wait()和 notify(),doWait()和 doNotify()里的信号还会被 2 个 MyWaitNotify 实例分别保存。在 MyWaitNotify1 上的一次 doNotify()调用可能唤醒 MyWaitNotify2 的线程,但是信号值只会保存在 MyWaitNotify1 里。
问题在于,由于 doNotify()仅调用了 notify()而不是 notifyAll(),即使有 4 个线程在相同的字符串(空字符串)实例上等待,只能有一个线程被唤醒。所以,如果线程 A 或 B 被发给 C 或 D 的信号唤醒,它会检查自己的信号值,看看有没有信号被接收到,然后回到等待状态。而 C 和 D 都没被唤醒来检查它们实际上接收到的信号值,这样信号便丢失了。这种情况相当于前面所说的丢失信号的问题。C 和 D 被发送过信号,只是都不能对信号作出回应。
doNotify() メソッドがnotify() の代わりにnotifyAll() を呼び出す場合、待機中のすべてのスレッドが起動され、順番にシグナル値をチェックします。スレッド A と B は待機状態に戻りますが、スレッド C または D の 1 つだけがシグナルに気づき、doWait() メソッド呼び出しを終了します。 C または D のもう 1 つは、シグナルを受け取ったスレッドが doWait() の終了時にシグナル値をクリア (false に設定) したため、待機状態に戻ります。
上記の段落を読んだ後、notify() の代わりに NoticeAll() を使用しようとするかもしれませんが、これはパフォーマンスの観点から悪い考えです。シグナルに応答できるスレッドが 1 つだけである場合、毎回すべてのスレッドを起動する必要はありません。
つまり: wait()/notify() メカニズムでは、グローバル オブジェクト、文字列定数などを使用しないでください。対応する一意のオブジェクトを使用する必要があります。たとえば、空の文字列で wait()/notify() を呼び出す代わりに、MyWaitNotify3 の各インスタンスには独自のモニター オブジェクトがあります。
上記はJavaマルチスレッドとスレッド通信に関する情報をまとめたものです。今後も関連情報を追加していきますので、どうぞよろしくお願いいたします。
その他の Java マルチスレッド間通信の例と関連記事については、PHP 中国語 Web サイトに注目してください。