明らかnotify
に、待機セット内の(任意の)スレッドを起動し、待機セット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
この状況が発生した場合に備えて、ループが必要です。
コンシューマー1(C1)が同期ブロックに入り、バッファーが空になるため、C1は(wait
呼び出しを介して)待機セットに入れられます。コンシューマー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
。なぜなら、それがtrueになるためには、以下がtrueでなければならないからです。
buf.size() == 0 AND buf.size() == MAX_SIZE
(MAX_SIZEが0ではないと想定)
ただし、これでは十分ではありませんnotifyAll
。使用する必要があります。理由を見てみましょう...
サイズが1のバッファーがあるとします(例をわかりやすくするため)。次の手順により、デッドロックが発生します。スレッドが通知でいつでも起こされることに注意してください。それは、JVMによって非決定的に選択される可能性があります。つまり、待機中のスレッドは起こされる可能性があります。また、メソッドへのエントリで複数のスレッドがブロックしている(つまり、ロックを取得しようとしている)場合、取得の順序が不定になる可能性があることに注意してください。また、スレッドは一度に1つのメソッドにしか存在できないことにも注意してください。同期されたメソッドでは、クラス内の任意の(同期された)メソッドを実行(つまり、ロックを保持)できるスレッドは1つだけです。次の一連のイベントが発生した場合-デッドロックが発生します。
ステップ1:
-P1は1文字をバッファに入れる
ステップ2:
-P2試行put
-待機ループをチェック-すでに文字-待機
ステップ3:
-P3試行put
-待機ループをチェック-すでに文字-待機
ステップ4:
-C1は1文字を取得しようとします-C2は1文字
を取得しようとします- get
メソッドへの入り口でブロックします
-C3は1文字を取得しようとします- get
メソッドへの入り口でブロックします
ステップ5:
-C1がget
メソッドを実行している-charを取得し、を呼び出しnotify
、メソッドを終了します
- notify
ウェイクアップP2-
ただし、P2がメソッドに入る前にP2がメソッドに入る(P2はロックを再取得する必要がある)ため、P2はput
メソッドへの入り口でブロックする
-C2待機ループをチェックし、バッファに文字がなくなるため待機します
-C3はC2の後、P2の前にメソッドに入り、待機ループをチェックし、バッファに文字がなくなるため待機します
ステップ6:
-今:P3、C2、C3が待機しています!
-最後にP2がロックを取得し、charをバッファに入れ、notifyを呼び出し、メソッドを終了します
ステップ7:
-P2の通知がP3を呼び起こします(スレッドは起こされる可能性があることに注意してください)-P3
は待機ループ条件をチェックします。バッファーにすでに文字があるため、待機します。
-通知を呼び出すスレッドがなくなり、3つのスレッドが永久に停止しました!
【解決交換notify
とnotifyAll
プロデューサ/コンシューマ・コード(上記)です。