明らかにnotify
、待機セット内の (任意の) 1 つのスレッドをnotifyAll
ウェイクし、待機セット内のすべてのスレッドをウェイクします。次の議論は、疑問を解決するはずです。notifyAll
ほとんどの場合に使用する必要があります。どちらを使用すればよいかわからない場合は、 を使用してnotifyAll
ください。以下の説明を参照してください。
非常に注意深く読んで理解してください。ご不明な点がございましたら、メールでお問い合わせください。
プロデューサー/コンシューマーを見てください (仮定は 2 つのメソッドを持つ ProducerConsumer クラスです)。IT IS BROKEN (使用しているためnotify
) - はい、動作する可能性があります - ほとんどの場合でも動作しますが、デッドロックを引き起こす可能性もあります - その理由は次のとおりです。
public synchronized void put(Object o) {
while (buf.size()==MAX_SIZE) {
wait(); // called if the buffer is full (try/catch removed for brevity)
}
buf.add(o);
notify(); // called in case there are any getters or putters waiting
}
public synchronized Object get() {
// Y: this is where C2 tries to acquire the lock (i.e. at the beginning of the method)
while (buf.size()==0) {
wait(); // called if the buffer is empty (try/catch removed for brevity)
// X: this is where C1 tries to re-acquire the lock (see below)
}
Object o = buf.remove(0);
notify(); // called if there are any getters or putters waiting
return o;
}
まず、
待機の前後に while ループが必要なのはなぜですか?
while
この状況が発生した場合に備えて、ループが必要です。
wait
コンシューマー 1 (C1) は同期ブロックに入り、バッファーは空であるため、C1 は (呼び出しを介して) 待機セットに入れられます。コンシューマー 2 (C2) は同期メソッドに入ろうとしていますが (上記の Y の時点)、プロデューサー P1 はオブジェクトをバッファーに入れ、続いて を呼び出しますnotify
。唯一の待機中のスレッドは C1 であるため、起動され、ポイント X (上記) でオブジェクト ロックを再取得しようとします。
ここで、C1 と C2 が同期ロックを取得しようとしています。そのうちの 1 つが (非決定論的に) 選択されてメソッドに入り、もう 1 つはブロックされます (待機していませんが、ブロックされ、メソッドのロックを取得しようとしています)。C2 が最初にロックを取得するとします。C1 はまだブロックしています (X でロックを取得しようとしています)。C2 はメソッドを完了し、ロックを解放します。ここで、C1 がロックを取得します。while
C1 がループ チェック (ガード) を実行し、バッファから存在しない要素を削除することを防がれている (C2 は既にそれを取得している!) ため、ループがあるのは幸運なことだと思います。がなかった場合、C1 がバッファから最初の要素を削除しようとすると、 が取得されますwhile
。IndexArrayOutOfBoundsException
今、
さて、なぜnotifyAllが必要なのでしょうか?
上記の生産者/消費者の例では、notify
. プロデューサーとコンシューマーの待機ループのガードが相互に排他的であることを証明できるため、このように見えます。つまり、put
メソッドだけでなくメソッドでもスレッドを待機させることはできないようです。これget
が真であるためには、次のことが真である必要があるためです。
buf.size() == 0 AND buf.size() == MAX_SIZE
(MAX_SIZE は 0 ではないと仮定)
しかし、これでは十分ではなく、 を使用する必要がありますnotifyAll
。理由を見てみましょう...
サイズ 1 のバッファーがあるとします (例をわかりやすくするため)。次の手順は、デッドロックにつながります。スレッドが通知で起動されるときはいつでも、JVM によって非決定論的に選択される可能性があることに注意してください。つまり、待機中のスレッドを起動することができます。また、複数のスレッドがメソッドへのエントリでブロックしている (つまり、ロックを取得しようとしている) 場合、取得の順序が非決定論的である可能性があることに注意してください。また、スレッドは一度に 1 つのメソッドにしか存在できないことに注意してください。同期メソッドでは、クラス内の任意の (同期された) メソッドを実行 (つまり、ロックを保持) できるスレッドは 1 つだけです。次の一連のイベントが発生すると、デッドロックが発生します。
ステップ 1:
- P1 が 1 文字をバッファーに入れる
ステップ 2:
- P2 の試行put
- 待機ループのチェック - すでに char - 待機
ステップ 3:
- P3 の試行put
- 待機ループのチェック - すでに char - 待機
ステップ 4:
- C1 は 1 文字の取得を
試みます - C2 は 1 文字の取得を試みます-get
メソッドへの入り口でブロックし
ます - C3 は 1 文字の取得を試みます - メソッドへの入り口でブロックしget
ます
ステップ 5:
- C1 はget
メソッドを実行しています - char を取得し、メソッドを呼び出します - P2 をウェイクアップしnotify
ます
- しかし
、C2 は P2 ができる前にメソッドに入ります (P2 はロックを再取得する必要があります)。そのため、P2 はメソッドへのエントリをブロックします
- C2待機ループをチェックし、バッファに文字がないため待機
- C3 は C2 の後、P2 の前にメソッドに入り、待機ループをチェックし、バッファに文字がないため待機しますnotify
put
ステップ 6:
- 今: P3、C2、C3 が待っています!
- 最後に P2 がロックを取得し、char をバッファに入れ、notify を呼び出し、メソッドを終了します。
ステップ 7:
- P2 の通知によって P3 がウェイクされます (任意のスレッドをウェイクアップできることに注意してください)
。
- NOTIFY を呼び出すスレッドがなくなり、3 つのスレッドが永久に中断されます。
解決策:プロデューサー/コンシューマー コード (上記)notify
をに置き換えます。notifyAll