Java: もう一度、notify() と notificationAll() を比較する 質問する

Java: もう一度、notify() と notificationAll() を比較する 質問する

Google で「notify()との違いnotifyAll()」と検索すると、多くの説明がポップアップ表示されます (javadoc の段落は別として)。結局のところ、起動される待機スレッドの数に帰着します。 では 1 つnotify()、 ではすべてのスレッドですnotifyAll()

ただし (これらの方法の違いを正しく理解していれば)、さらにモニターを取得するために選択されるスレッドは常に 1 つだけです。最初のケースでは VM によって選択されたスレッド、2 番目のケースではシステム スレッド スケジューラによって選択されたスレッドです。これら 2 つの方法の正確な選択手順 (一般的なケース) は、プログラマにはわかりません。

両者の便利な違いは何ですか?通知()そしてすべて通知()では?何か見逃しているのでしょうか?

ベストアンサー1

明らかに、notifyは待機セット内の (任意の) 1 つのスレッドを起動し、notifyAllは待機セット内のすべてのスレッドを起動します。次の説明により疑問が解消されるはずです。notifyAllほとんどの場合、 を使用する必要があります。どちらを使用するかわからない場合は、 を使用してくださいnotifyAll。以下の説明を参照してください。

よく読んで理解してください。質問がある場合はメールでお問い合わせください。

プロデューサー/コンシューマーを見てください (2 つのメソッドを持つ ProducerConsumer クラスを想定)。これは壊れています ( を使用しているため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 だけなので、C1 が起動され、ポイント X (上記) でオブジェクト ロックの再取得を試みます。

ここで、C1 と C2 は同期ロックを取得しようとしています。どちらか一方が (非決定的に) 選択されてメソッドに入り、もう一方はブロックされます (待機するのではなく、メソッドのロックを取得しようとしてブロックされます)。C2 が最初にロックを取得したとします。C1 はまだブロックしています (X でロックを取得しようとしています)。C2 はメソッドを完了し、ロックを解放します。これで、C1 がロックを取得します。幸運なことwhileに、ループがあります。C1 はループ チェック (ガード) を実行し、存在しない要素をバッファーから削除できないようにするからです (C2 はすでに取得しています)。 がない場合は、 C1 がバッファーから最初の要素を削除しようとすると がwhile発生します。IndexArrayOutOfBoundsException

今、

さて、なぜnotifyAllが必要なのでしょうか?

上記のプロデューサー/コンシューマーの例では、 で済むように見えます。プロデューサーとコンシューマーの待機notifyループのガードが相互に排他的であることを証明できるため、このように見えます。つまり、メソッドとメソッドの両方でスレッドを待機させることはできないようです。これが真実であるためには、次のことが真実でなければならないからです。putget

buf.size() == 0 AND buf.size() == MAX_SIZE(MAX_SIZE が 0 でないと仮定)

しかし、これでは十分ではありません。 を使用する必要がありますnotifyAll。 理由を見てみましょう...

サイズが 1 のバッファがあると仮定します (例をわかりやすくするため)。次の手順でデッドロックが発生します。notify でスレッドがウェイクアップされるときはいつでも、JVM によって非決定的に選択される可能性があることに注意してください。つまり、待機中のスレッドがウェイクアップされる可能性があります。また、複数のスレッドがメソッドへのエントリでブロックしている場合 (つまり、ロックを取得しようとしている場合)、取得の順序が非決定的になる可能性があることにも注意してください。また、スレッドは一度に 1 つのメソッドにしか存在できないことに注意してください。同期メソッドでは、クラス内の任意の (同期) メソッドを実行 (つまり、ロックを保持) できるスレッドは 1 つだけです。次の一連のイベントが発生すると、デッドロックが発生します。

ステップ1:
- P1は1文字をバッファに格納します

ステップ2:
- P2が試行put- 待機ループをチェック - すでに文字 - 待機

ステップ3:
- P3が試行put- 待機ループをチェック - すでに文字 - 待機

ステップ 4:
- C1 は 1 文字を取得しようとします
- C2 は 1 文字を取得しようとします - メソッドへのエントリでブロックされますget- C3 は 1 文字を取得しようとします -メソッド
へのエントリでブロックされますget

ステップ 5:
- C1 がgetメソッドを実行しています - 文字を取得し、を呼び出してnotify、メソッドを終了します
- はnotifyP2 を起動します
- ただし、C2 は P2 より先にメソッドに入ります (P2 はロックを再取得する必要がある) ので、P2 はメソッドに入るときにブロックしますput-
C2 は待機ループをチェックし、バッファに文字がもうないので待機します
- C3 は C2 より後、P2 より前にメソッドに入り、待機ループをチェックし、バッファに文字がもうないので待機します

ステップ6:
- 現在: P3、C2、C3が待機しています。
- 最後にP2がロックを取得し、バッファに文字を入れ、通知を呼び出し、メソッドを終了します。

ステップ 7:
- P2 の通知により P3 が起動します (どのスレッドも起動できることに注意してください)
- P3 は待機ループの状態をチェックし、バッファーにすでに文字があるため待機します。
- NOTIFY を呼び出すスレッドがなくなり、3 つのスレッドが永続的に中断されます。

解決策:プロデューサー/コンシューマー コード (上記) 内のnotifyを に置き換えます。notifyAll

おすすめ記事