ジャバ:Notice() ともう一度notifyAll()をやり直す
-
09-06-2019 - |
質問
Google で「次の違い」を検索すると、 notify()
そして notifyAll()
" そうすると、(Javadoc の段落は別にして) 多くの説明が表示されます。すべては、ウェイクアップされている待機中のスレッドの数に要約されます。1つ中 notify()
そしてすべて notifyAll()
.
ただし、(これらのメソッドの違いを正しく理解していれば)、さらなるモニター取得のために選択されるスレッドは常に 1 つだけです。前者の場合は VM によって選択されたもの、後者の場合はシステム スレッド スケジューラによって選択されたものです。両方の正確な選択手順 (一般的な場合) はプログラマにはわかりません。
解決
ただし、(これらのメソッドの違いを正しく理解していれば)、以降のモニター取得には常に 1 つのスレッドのみが選択されます。
それは正しくありません。 o.notifyAll()
目覚める 全て でブロックされているスレッドの数 o.wait()
呼び出します。スレッドは次からのみ戻ることができます。 o.wait()
一つずつですが、それぞれ 意思 順番をもらいます。
簡単に言うと、スレッドが通知を待っている理由によって異なります。待機中のスレッドの 1 つに何かが起こったことを伝えたいですか、それともすべてのスレッドに同時に伝えたいですか?
場合によっては、待機が終了すると、待機中のすべてのスレッドが有益なアクションを実行できることがあります。例としては、特定のタスクが終了するのを待っている一連のスレッドが挙げられます。タスクが完了すると、待機中のすべてのスレッドは業務を続行できます。そのような場合に使用するのは すべて通知() 待機中のすべてのスレッドを同時に起動します。
別のケース (相互排他ロックなど) では、通知を受けた後、待機中のスレッドの 1 つだけが有益な操作を行うことができます (この場合はロックを取得します)。そのような場合には、むしろ使用するとよいでしょう 通知(). 。適切に実装されれば、 できた 使用 すべて通知() この状況でも同様ですが、とにかく何もできないスレッドを不必要にウェイクアップすることになります。
多くの場合、条件を待つコードはループとして記述されます。
synchronized(o) {
while (! IsConditionTrue()) {
o.wait();
}
DoSomethingThatOnlyMakesSenseWhenConditionIsTrue_and_MaybeMakeConditionFalseAgain();
}
そうすれば、 o.notifyAll()
呼び出しは複数の待機スレッドをウェイクアップし、最初のスレッドがスレッドから戻ります。 o.wait()
条件を false 状態のままにすると、起動されていた他のスレッドは待機状態に戻ります。
他のヒント
明らかに、 notify
待機セット内の (任意の) 1 つのスレッドをウェイクアップします。 notifyAll
待機セット内のすべてのスレッドをウェイクアップします。以下の議論で疑問が解消されるはずです。 notifyAll
ほとんどの場合に使用する必要があります。どれを使用すればよいかわからない場合は、次を使用してください notifyAll
. 以下の説明を参照してください。
非常に注意深く読んで理解してください。ご質問がございましたら、メールをお送りください。
プロデューサー/コンシューマーを見てください (2 つのメソッドを持つプロデューサーコンシューマー クラスであると仮定します)。壊れています(使用しているため) 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
電話)。Consumer 2 (C2) は (上記の点 Y で) 同期メソッドに入ろうとしていますが、Producer P1 はオブジェクトをバッファーに入れ、その後呼び出します。 notify
. 。唯一の待機スレッドは C1 なので、ウェイクアップされ、ポイント X (上記) でオブジェクト ロックを再取得しようとします。
現在、C1 と C2 は同期ロックを取得しようとしています。そのうちの 1 つが (非決定的に) 選択されてメソッドに入り、もう 1 つはブロックされます (待機ではなくブロックされ、メソッドのロックを取得しようとします)。C2 が最初にロックを取得したとします。C1 はまだブロック中です (X でロックを取得しようとしています)。C2 はメソッドを完了し、ロックを解放します。ここで、C1 がロックを取得します。幸運なことに、私たちは while
なぜなら、C1 はループ チェック (ガード) を実行し、存在しない要素をバッファから削除することができないからです (C2 はすでにそれを取得しています!)。もし私たちが持っていなかったら while
, を取得します。 IndexArrayOutOfBoundsException
C1 がバッファから最初の要素を削除しようとしているためです。
今、
さて、なぜ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
- 待機ループをチェックします - すでに文字です - 待機します
ステップ 3:
- P3 の試行 put
- 待機ループをチェックします - すでに文字です - 待機します
ステップ4:
- C1 は 1 文字の取得を試みます
- C2 は 1 文字を取得しようとします - へのエントリでブロックします get
方法
- C3 は 1 文字を取得しようとします - へのエントリでブロックします get
方法
ステップ5:
- C1 が実行中 get
メソッド - 文字を取得し、呼び出します notify
, 、メソッドを終了します
- notify
目を覚ますP2
- ただし、P2 より先に C2 がメソッドに入る (P2 はロックを再取得する必要がある) ため、P2 はメソッドへのエントリをブロックします。 put
方法
- C2 は待機ループをチェックし、バッファーに文字がなくなったので待機します。
- C3 は C2 の後、P2 より前にメソッドに入り、待機ループをチェックし、バッファーに文字がないため待機します。
ステップ6:
- 今:P3、C2、C3が待っています!
- 最後に、P2 はロックを取得し、バッファに char を置き、notify を呼び出し、メソッドを終了します。
ステップ7:
- P2 の通知により P3 が起動されます (どのスレッドも起動できることに注意してください)
- P3 は待機ループ条件をチェックします。バッファ内にすでに char があるため、待機します。
- 通知を呼び出すスレッドがなくなり、3 つのスレッドが永久に停止されました。
解決:交換する notify
と notifyAll
プロデューサ/コンシューマ コード (上記)。
有用な違い:
使用 通知() すべての待機スレッドが交換可能である場合 (起動順序は重要ではありません)、または待機スレッドが 1 つしかない場合。一般的な例は、キューからジョブを実行するために使用されるスレッド プールです。ジョブが追加されると、スレッドの 1 つが起動し、次のジョブを実行してスリープに戻るように通知されます。
使用 すべて通知() 待機中のスレッドに異なる目的があり、同時に実行できる必要があるその他の場合。例としては、共有リソースに対するメンテナンス操作が挙げられます。この場合、複数のスレッドがリソースにアクセスする前に操作の完了を待機します。
それは資源がどのように生産され消費されるかによると思います。5 つの作業オブジェクトが同時に使用可能で、5 つのコンシューマー オブジェクトがある場合、notifyAll() を使用してすべてのスレッドを起動し、各スレッドが 1 つの作業オブジェクトを処理できるようにするのが合理的です。
使用可能な作業オブジェクトが 1 つだけの場合、すべてのコンシューマー オブジェクトを起動して、その 1 つのオブジェクトを求めて競合することに何の意味があるでしょうか?利用可能な作業をチェックする最初のスレッドがそれを取得し、他のすべてのスレッドがチェックして、何もすることがないことがわかります。
見つけました ここに素晴らしい説明があります. 。要するに:
notify()メソッドは通常使用されます リソースプール, 、リソースを取得する「消費者」または「労働者」の任意の数があるが、リソースがプールに追加されると、待っている消費者または労働者の1人だけが対処できる。notifyall()メソッドは、他のほとんどの場合に実際に使用されます。厳密には、複数のウェイターが進むことができる状態をウェイターに通知する必要があります。しかし、これはしばしば知るのが難しいです。一般的なルールとして、 notify()を使用するための特定のロジックがない場合は、おそらくnotifyall()を使用する必要があります、特定のオブジェクトでどのスレッドが待っているのか、そしてその理由を正確に知ることはしばしば難しいからです。
同時実行ユーティリティを使用すると、次のいずれかを選択できることに注意してください。 signal()
そして signalAll()
これらのメソッドがそこで呼び出されるためです。したがって、次の場合でも質問は有効のままです java.util.concurrent
.
ダグ・リーは彼の著書で興味深い点を指摘しています。 有名な本:もし notify()
そして Thread.interrupt()
同時に発生すると、通知が実際に失われる可能性があります。これが起こり得て劇的な影響を与える場合 notifyAll()
オーバーヘッド (ほとんどの場合、起動するスレッドが多すぎる) という代償を払ったとしても、これはより安全な選択です。
『Effective Java 第 2 版』の Java 達人、Joshua Bloch 氏より:
「項目69:並行性ユーティリティが待機して通知することを優先します。」
ここに一例を示します。それを実行します。次に、notifyAll() の 1 つを Notice() に変更して、何が起こるかを確認します。
ProducerConsumerExample クラス
public class ProducerConsumerExample {
private static boolean Even = true;
private static boolean Odd = false;
public static void main(String[] args) {
Dropbox dropbox = new Dropbox();
(new Thread(new Consumer(Even, dropbox))).start();
(new Thread(new Consumer(Odd, dropbox))).start();
(new Thread(new Producer(dropbox))).start();
}
}
ドロップボックスクラス
public class Dropbox {
private int number;
private boolean empty = true;
private boolean evenNumber = false;
public synchronized int take(final boolean even) {
while (empty || evenNumber != even) {
try {
System.out.format("%s is waiting ... %n", even ? "Even" : "Odd");
wait();
} catch (InterruptedException e) { }
}
System.out.format("%s took %d.%n", even ? "Even" : "Odd", number);
empty = true;
notifyAll();
return number;
}
public synchronized void put(int number) {
while (!empty) {
try {
System.out.println("Producer is waiting ...");
wait();
} catch (InterruptedException e) { }
}
this.number = number;
evenNumber = number % 2 == 0;
System.out.format("Producer put %d.%n", number);
empty = false;
notifyAll();
}
}
コンシューマクラス
import java.util.Random;
public class Consumer implements Runnable {
private final Dropbox dropbox;
private final boolean even;
public Consumer(boolean even, Dropbox dropbox) {
this.even = even;
this.dropbox = dropbox;
}
public void run() {
Random random = new Random();
while (true) {
dropbox.take(even);
try {
Thread.sleep(random.nextInt(100));
} catch (InterruptedException e) { }
}
}
}
プロデューサークラス
import java.util.Random;
public class Producer implements Runnable {
private Dropbox dropbox;
public Producer(Dropbox dropbox) {
this.dropbox = dropbox;
}
public void run() {
Random random = new Random();
while (true) {
int number = random.nextInt(10);
try {
Thread.sleep(random.nextInt(100));
dropbox.put(number);
} catch (InterruptedException e) { }
}
}
}
短い要約:
常に優先します すべて通知() 以上 通知() ただし、多数のスレッドがすべて同じことを実行する超並列アプリケーションがある場合は除きます。
説明:
通知() ...] 1つのスレッドを目覚めさせます。なぜなら 通知() 目が覚めたスレッドを指定することはできません。つまり、非常に並行してアプリケーション、つまり多数のスレッドを備えたプログラムでのみ役立ち、すべてが同様の雑用を行っています。このようなアプリケーションでは、どのスレッドが起動されるかは気にしません。
ソース: https://docs.oracle.com/javase/tutorial/essential/concurrency/guardmeth.html
比較する 通知() と すべて通知() 上記の状況では:スレッドが同じことを実行する超並列アプリケーション。電話したら すべて通知() その場合、 すべて通知() 目覚めを誘発します(すなわち、膨大な数のスレッドのスケジューリング)、その多くは不必要です(実際に進行できるのは 1 つのスレッド、つまりオブジェクトのモニターを許可されるスレッドだけであるため) 待って(), 通知(), 、 または すべて通知() が呼び出されました)、したがってコンピューティング リソースを無駄に消費します。
したがって、膨大な数のスレッドが同じことを同時に実行するアプリケーションがない場合は、 すべて通知() 以上 通知(). 。なぜ?なぜなら、他のユーザーがこのフォーラムですでに回答しているように、 通知()
このオブジェクトのモニターで待機している単一のスレッドを起動します。...]選択はです 任意 実装の裁量で発生します。
ソース:Java SE8 API (https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#notify--)
コンシューマの準備ができているプロデューサー コンシューマ アプリケーションがあると想像してください (つまり、 待って() )消費する準備ができている(すなわち、 待って() ing) が生産され、アイテム (生産/消費される) のキューは空です。その場合、 通知() 消費者のみが目覚め、生産者は目覚めない可能性があります。なぜなら、誰を目覚めさせるかは選択によるからです。 任意. 。生産者と消費者はそれぞれ生産と消費の準備ができていますが、生産者と消費者のサイクルは何の進歩もありません。代わりに、消費者は目覚めます(つまり、を離れる 待って() ステータス)、キューが空であるため項目をキューから取り除きません。 通知() 別の消費者が続行します。
対照的に、 すべて通知() 生産者と消費者の両方を目覚めさせます。誰をスケジュールするかはスケジューラによって異なります。もちろん、スケジューラの実装によっては、スケジューラはコンシューマのみをスケジュールすることもあります (例:コンシューマー スレッドに非常に高い優先順位を割り当てた場合)。ただし、ここでの前提は、スケジューラがコンシューマのみをスケジュールする危険性は、JVM がコンシューマをウェイクアップするだけの危険性よりも低いということです。 任意 決断。むしろ、ほとんどのスケジューラ実装では、飢餓を防ぐために少なくともある程度の努力が行われます。
悪名高い「ロストウェイクアップ」問題について誰も言及しなかったことに非常に驚きました(グーグルで調べてください)。
基本的に:
- 複数のスレッドが同じ条件で待機している場合、
- 状態 A から状態 B に遷移できる複数のスレッド、および
- 状態 B から状態 A に遷移できる複数のスレッド (通常は 1. と同じスレッド)、そして、
- 状態 A から B への遷移は、1 のスレッドに通知する必要があります。
その後、スリープ解除が不可能であるという証明可能な保証がない限り、notifyAll を使用する必要があります。
一般的な例は、次のような同時 FIFO キューです。複数のエンキュー (1.そして3.上記)は、キューを空から空の複数のデクイアーから非空白から非拡張することができます(2。上記)条件を待つことができます。
空のキューから開始して 2 つのエンキューと 2 つのデキューが対話し、1 つのエンキューがスリープ状態を維持する操作のインターリーブを簡単に作成できます。
これはおそらくデッドロック問題に匹敵する問題です。
これでいくつかの疑問が解消されることを願っています。
通知() :notify()メソッドは、ロックを待っている1つのスレッド(そのロックのwait()を呼び出す最初のスレッド)を目覚めさせます。
すべて通知() :NoticeAll() メソッドは、ロックを待機しているすべてのスレッドを起動します。JVMは、ロックを待っているスレッドのリストからスレッドの1つを選択し、そのスレッドを目覚めさせます。
シングルスレッドの場合 ロックを待機している場合、notify() とnotifyAll() の間に大きな違いはありません。ただし、ロックを待機しているスレッドが複数ある場合、notify() と NoticeAll() の両方で、ウェイクアップされる正確なスレッドは次のとおりです。 JVMの制御下にある また、特定のスレッドの起動をプログラムで制御することはできません。
一見すると、notify() を呼び出して 1 つのスレッドを起動するのが良いように見えます。すべてのスレッドを起動する必要はないと思われるかもしれません。ただし、問題は、 Notice() は、ウェイクアップされたスレッドが適切なスレッドではない可能性があることを示します (スレッドが他の条件を待っているか、そのスレッドの条件がまだ満たされていない可能性があります)。 その場合, 場合、notify() が失われ、他のスレッドが起動しなくなり、一種のデッドロックが発生する可能性があります (通知が失われ、他のすべてのスレッドが通知を永遠に待機することになります)。
この問題を回避するには, 、ロックを待機しているスレッドが複数ある場合 (または待機が完了する条件が複数ある場合)、notifyAll() を呼び出すことをお勧めします。NoticeAll() メソッドはすべてのスレッドを起動するため、あまり効率的ではありません。ただし、実際のアプリケーションではこのパフォーマンスの損失は無視できます。
notify()
その間、1つのスレッドを起動します notifyAll()
全員が目を覚ますでしょう。私の知る限り、中間点はありません。しかし、何かわからない場合は、 notify()
スレッドに実行します。使用してください notifyAll()
. 。いつでも魅力的に機能します。
私の知る限り、上記の答えはすべて正しいので、別のことをお話しします。実稼働コードの場合は、java.util.concurrent のクラスを使用する必要があります。Java の同時実行性の分野では、彼らにできないことはほとんどありません。
より簡単な説明は次のとおりです。
あなたの言うとおり、notify() を使用するか、notifyAll() を使用するかに関係なく、即座の結果として、他の 1 つのスレッドがモニターを取得して実行を開始します。(一部のスレッドがこのオブジェクトの wait() で実際にブロックされていたと仮定すると、他の無関係なスレッドが使用可能なコアをすべて吸収しているわけではありません。など) 影響は後で発生します。
スレッド A、B、および C がこのオブジェクトを待機していて、スレッド A がモニターを取得するとします。違いは、A がモニターを放した後に何が起こるかにあります。Notice() を使用した場合、B と C は依然として wait() でブロックされます。彼らはモニターを待っているのではなく、通知を待っているのです。A がモニターを解放しても、B と C は依然としてそこに留まり、notify() を待っています。
NoticeAll() を使用した場合、B と C は両方とも「通知待ち」状態を超えて、モニターの取得を待機しています。A がモニターを解放すると、B または C のいずれかがそれを取得し (そのモニターをめぐって競合するスレッドが他にないと仮定して)、実行を開始します。
スレッドには 3 つの状態があります。
- WAIT - スレッドは CPU サイクルを使用していません
- BLOCKED - スレッドはモニターを取得しようとしてブロックされています。まだ CPU サイクルを使用している可能性があります。
- 実行中 - スレッドは実行中です。
これで、notify() が呼び出されると、JVM は 1 つのスレッドを選択して BLOCKED 状態に移行し、モニター オブジェクトの競合がないため RUNNING 状態に移行します。
NoticeAll() が呼び出されると、JVM はすべてのスレッドを選択し、すべてを BLOCKED 状態に移行します。これらすべてのスレッドは優先順位に基づいてオブジェクトのロックを取得します。最初にモニターを取得できたスレッドが最初に RUNNING 状態に移行でき、以下同様になります。
この回答は、による優れた回答をグラフィカルに書き直して簡略化したものです。 ザギグ, によるコメントを含む エラン.
各製品が単一の消費者を対象としている場合でも、なぜnotifyAllを使用するのでしょうか?
次のように単純化して、生産者と消費者を考えてみましょう。
プロデューサー:
while (!empty) {
wait() // on full
}
put()
notify()
消費者:
while (empty) {
wait() // on empty
}
take()
notify()
2 つのプロデューサーと 2 つのコンシューマーがサイズ 1 のバッファーを共有すると仮定します。次の図は、 デッドロック, 、すべてのスレッドが使用されている場合は回避されます すべてに通知する.
各通知には、ウェイクアップされているスレッドのラベルが付けられます。
notify()
より効率的なコードを書くことができます。 notifyAll()
.
複数の並列スレッドから実行される次のコード部分を考えてみましょう。
synchronized(this) {
while(busy) // a loop is necessary here
wait();
busy = true;
}
...
synchronized(this) {
busy = false;
notifyAll();
}
を使用するとさらに効率化できます notify()
:
synchronized(this) {
if(busy) // replaced the loop with a condition which is evaluated only once
wait();
busy = true;
}
...
synchronized(this) {
busy = false;
notify();
}
スレッドの数が多い場合、または待機ループ条件の評価にコストがかかる場合は、 notify()
よりも大幅に高速になります notifyAll()
. 。たとえば、1000 個のスレッドがある場合、最初のスレッドの後に 999 個のスレッドが起動されて評価されます。 notifyAll()
, 、次に 998、次に 997 というように続きます。逆に、 notify()
解決策としては、1 つのスレッドだけが起動されます。
使用 notifyAll()
次にどのスレッドが作業を行うかを選択する必要がある場合:
synchronized(this) {
while(idx != last+1) // wait until it's my turn
wait();
}
...
synchronized(this) {
last = idx;
notifyAll();
}
最後に、次のような場合に理解することが重要です。 notifyAll()
, 、中のコード synchronized
起動されたブロックは、一度にすべてではなく、順番に実行されます。上記の例では 3 つのスレッドが待機していて、4 番目のスレッドが呼び出しを行ったとします。 notifyAll()
. 。3 つのスレッドはすべて起動されますが、実行を開始してスレッドの状態を確認するのは 1 つだけです。 while
ループ。条件が true
, 、それは呼びます wait()
再度実行すると、2 番目のスレッドが実行を開始し、そのスレッドがチェックされます。 while
ループ条件など。
から引用 ブログ 効果的な Java について:
The notifyAll method should generally be used in preference to notify.
If notify is used, great care must be taken to ensure liveness.
それで、私が理解していることは次のとおりです(前述のブログ、「Yann TM」によるコメントより) 受け入れられた回答 とJava ドキュメント):
- 通知() :JVM は、このオブジェクト上で待機しているスレッドの 1 つを起動します。スレッドの選択は公平性なく恣意的に行われます。したがって、同じスレッドが何度も起動される可能性があります。したがって、システムの状態は変化しますが、実際の進歩は見られません。したがって、 ライブロック.
- 通知すべて() :JVM がすべてのスレッドを起動すると、すべてのスレッドがこのオブジェクトのロックを競合します。ここで、CPU スケジューラは、このオブジェクトのロックを取得するスレッドを選択します。この選択プロセスは、JVM による選択よりもはるかに優れています。したがって、生存性が確保されます。
@xagyg によって投稿されたコードを見てください。
2 つの異なるスレッドが 2 つの異なる条件を待っているとします。
の 最初のスレッド 待っています buf.size() != MAX_SIZE
, 、 そしてその 2番目のスレッド 待っています buf.size() != 0
.
ある時点で仮定してください buf.size()
0に等しくありません. 。JVM呼び出し notify()
の代わりに notifyAll()
, 、最初のスレッドに通知されます (2 番目のスレッドではありません)。
最初のスレッドが起動され、次のことをチェックします。 buf.size()
戻ってくるかもしれない MAX_SIZE
, 、そして待機に戻ります。2 番目のスレッドはウェイクアップされず、待機し続け、呼び出しません。 get()
.
notify()
呼び出した最初のスレッドを起動します wait()
同じオブジェクト上で。
notifyAll()
呼び出したすべてのスレッドを起動します wait()
同じオブジェクト上で。
最も優先度の高いスレッドが最初に実行されます。
「Java Concurrency in Practice」で説明されている内容について触れておきたいと思います。
最初のポイントは、Notify か NotifyAll ですか?
It will be NotifyAll, and reason is that it will save from signall hijacking.
2つのスレッドAとBが異なる条件で同じ条件キューの述語を待っており、通知が呼び出された場合、JVMが通知するのはJVMまでです。
NotifyがスレッドAとJVM通知スレッドB用に意図されている場合、スレッドBは目を覚まし、この通知が役に立たないことを確認してください。そして、スレッドAはこの見逃した信号について知るようになることはなく、誰かが通知をハイジャックしました。
したがって、NotifyAllを呼び出すとこの問題が解決されますが、すべてのスレッドに通知されるため、パフォーマンスに影響を与え、すべてのスレッドが同じロックを競合し、コンテキストスイッチとCPUの負荷が含まれます。しかし、動作自体が正しくない場合にのみ、パフォーマンスが正しく動作している場合にのみパフォーマンスを気にする必要があります。パフォーマンスは役に立ちません。
この問題は、jdk 5 で提供される明示的ロック Lock の Condition オブジェクトを使用することで解決できます。条件述語ごとに異なる待機が提供されるためです。ここでは正しく動作し、シグナルを呼び出してその条件を待っているスレッドが 1 つだけであることを確認するため、パフォーマンスの問題は発生しません。
Notice は待機状態にある 1 つのスレッドのみに通知しますが、notify all は待機状態にあるすべてのスレッドに通知します。通知されたすべてのスレッドとブロックされたすべてのスレッドがロックの対象となり、そのうち 1 つだけがロックを取得します。他のすべて (以前に待機状態になっていた人を含む) はブロック状態になります。
notify()
- オブジェクトの待機セットからランダムなスレッドを選択し、それを BLOCKED
州。オブジェクトの待機セット内の残りのスレッドはまだ WAITING
州。
notifyAll()
- すべてのスレッドをオブジェクトの待機セットから BLOCKED
州。使用後は notifyAll()
, 、共有オブジェクトの待機セットにはスレッドがすべて残っているので、スレッドはすべて待機セットに残っています。 BLOCKED
状態ではない WAITING
州。
BLOCKED
- ロックの取得がブロックされています。WAITING
- 通知を待機中 (または参加完了のためにブロックされています)。
上記の優れた詳細な説明を要約すると、私が考えることができる最も単純な方法で言えば、これは、1) 同期単位 (ブロックまたはオブジェクト) 全体で取得される、2) JVM 組み込みモニターの制限によるものです。待機/通知される特定の条件を区別しません。
これは、複数のスレッドが異なる条件で待機しており、notify() が使用されている場合、選択されたスレッドが、新たに満たされた条件で処理を進めるスレッドではない可能性があり、そのスレッド (および現在待機中の他のスレッドが処理を続行できる可能性がある) を引き起こすことを意味します。条件を満たすなど) 先に進むことができなくなり、最終的には飢餓やプログラムのハングアップが発生します。
対照的に、notifyAll() を使用すると、待機中のすべてのスレッドが最終的にロックを再取得し、それぞれの状態をチェックできるため、最終的には処理を進めることができます。
したがって、notify() は、待機中のスレッドが選択された場合に進行を許可することが保証されている場合にのみ安全に使用できます。一般に、これは、同じモニター内のすべてのスレッドが 1 つの同じ条件のみをチェックするときに満たされますが、これはかなりまれです。現実世界のアプリケーションの場合。
「オブジェクト」の wait() を呼び出すと (オブジェクトのロックが取得されると予想されます)、これによりそのオブジェクトのロックが解放され、他のスレッドがこの「オブジェクト」をロックできるようになります。このシナリオでは、 「リソース/オブジェクト」を待機しているスレッドが 1 つ以上あります (他のスレッドも上記の同じオブジェクトに対して待機を発行し、その先にはリソース/オブジェクトを埋めて notify/notifyAll を呼び出すスレッドが存在することを考慮します)。
ここで、(プロセス/コードの同じ側/反対側から)同じオブジェクトの通知を発行すると、ブロックされ待機中の単一スレッドが解放されます(待機中のすべてのスレッドではなく、この解放されたスレッドはJVMスレッドによって選択されます)スケジューラとオブジェクトに対するすべてのロック取得プロセスは通常と同じです)。
このオブジェクトを共有/作業するスレッドが 1 つだけの場合は、wait-notify 実装で notify() メソッドを単独で使用しても問題ありません。
ビジネスロジックに基づいて複数のスレッドがリソース/オブジェクトの読み取りと書き込みを行う状況にある場合は、notifyAll() を使用する必要があります。
今、オブジェクトに対してnotify()を発行するときに、JVMが待機中のスレッドをどのように正確に識別して中断しているかを調べています...
上記には確かな答えがいくつかありますが、これまで読んだ混乱や誤解の多さに驚きました。これはおそらく、壊れた同時実行コードを独自に作成しようとするのではなく、可能な限り java.util.concurrent を使用する必要があるという考えを証明しています。質問に戻ります。要約すると、現時点でのベスト プラクティスは、ウェイクアップの喪失の問題に起因するすべての状況で、notify() を避けることです。これを理解していない人には、ミッションクリティカルな同時実行コードを作成させるべきではありません。ハーディングの問題が心配な場合は、一度に 1 つのスレッドをウェイクアップする安全な方法の 1 つは次のとおりです。1.待機スレッド用の明示的な待機キューを構築します。2.キュー内の各スレッドに、その先行スレッドを待機させます。3.完了したら、各スレッドがnotifyAll()を呼び出すようにします。または、すでにこれを実装している Java.util.concurrent.* を使用することもできます。
ここでは全員が目を覚ますことはあまり意味がありません。wait Notice と Notifyall、これらはすべて、オブジェクトのモニターを所有した後に配置されます。スレッドが待機段階にあり、notify が呼び出された場合、このスレッドがロックを取得し、その時点で他のスレッドがそのロックを取得することはできません。したがって、同時アクセスはまったく実行できません。私の知る限り、wait Notice および Notifyall の呼び出しは、オブジェクトのロックを取得した後にのみ実行できます。私が間違っていたら訂正してください。