web-dev-qa-db-ja.com

wait()が常に同期ブロックにある必要がある理由

Object.wait() を呼び出すには、この呼び出しを同期ブロックに配置する必要があります。それ以外の場合は IllegalMonitorStateException がスローされます。しかし、この制限を行う理由は何ですか?wait()がモニターを解放することは知っていますが、特定のブロックを同期させてからモニターを明示的に取得してから、 wait()を呼び出していますか?

同期ブロックの外でwait()を呼び出して、そのセマンティクスを保持することができた場合、潜在的な損害は何ですか?呼び出し元スレッドを中断しますか?

245
diy

wait()は、notify()もある場合にのみ意味をなすため、スレッド間の通信が常に重要であり、正しく動作するには同期が必要です。これは暗黙的であるべきだと主張することもできますが、次の理由でそれは実際には役に立たないでしょう:

意味的には、wait()だけではありません。満足するためには何らかの条件が必要であり、そうでない場合は、そうなるまで待ちます。あなたが本当にやることは

if(!condition){
    wait();
}

しかし、条件は別のスレッドによって設定されているため、これを正しく機能させるためには同期が必要です。

スレッドが待機を終了したからといって、探している条件が真であるとは限らないという点で、さらにいくつか間違っています。

  • スプリアスウェイクアップを受け取ることができます(スレッドが通知を受信せずに待機状態からウェイクアップできることを意味します)、または

  • 条件は設定できますが、3番目のスレッドは、待機中のスレッドがウェイクアップする(およびモニターを再取得する)までに条件を再び偽にします。

これらのケースに対処するために本当に必要なのはalwaysのバリエーションです:

synchronized(lock){
    while(!condition){
        lock.wait();
    }
}

いっそのこと、同期プリミティブをまったく混乱させず、Java.util.concurrentパッケージで提供される抽象化を使用してください。

220

同期ブロックの外でwait()を呼び出し、そのセマンティクスを保持することができた場合の潜在的な損傷は何ですか?-呼び出し元スレッドを中断しますか?

具体例を使用して、wait()を同期ブロックの外側で呼び出すことができた場合にどのような問題に遭遇するかを説明しましょう。

ブロッキングキューを実装するとします(APIには既に1つあります:)

最初の試行(同期なし)は、以下の行に沿って何かを見る可能性があります

class BlockingQueue {
    Queue<String> buffer = new LinkedList<String>();

    public void give(String data) {
        buffer.add(data);
        notify();                   // Since someone may be waiting in take!
    }

    public String take() throws InterruptedException {
        while (buffer.isEmpty())    // don't use "if" due to spurious wakeups.
            wait();
        return buffer.remove();
    }
}

これは潜在的に起こりうることです:

  1. コンシューマスレッドはtake()を呼び出し、buffer.isEmpty()を確認します。

  2. コンシューマスレッドがwait()を呼び出す前に、プロデューサスレッドが来て、完全なgive()、つまりbuffer.add(data); notify();を呼び出します。

  3. コンシューマスレッドは、wait()(およびmiss今呼び出されたnotify())を呼び出すようになります。

  4. 運が悪ければ、プロデューサースレッドはコンシューマスレッドが起動しないという事実の結果として、さらにgive()を生成せず、デッドロックが発生します。

問題を理解したら、解決策は明白です。synchronizedを使用して、notifyisEmptyの間でwaitが呼び出されないようにします。

詳細に入ることなく:この同期の問題は普遍的です。 Michael Borgwardtが指摘するように、wait/notifyはスレッド間の通信に関するものなので、常に上記のような競合状態に陥ります。これが「同期内でのみ待機」ルールが実施される理由です。


@ Willieによって投稿されたリンク からの段落は、それを非常にうまく要約しています:

ウェイターとノーティファイアーが述部の状態について同意するという絶対的な保証が必要です。ウェイターは、スリープに入る前に、ある時点で述語の状態をチェックしますが、スリープに入るときに述語が正しいかどうかに依存します。これらの2つのイベントの間には脆弱性の期間があり、プログラムを破壊する可能性があります。

プロデューサーとコンシューマーが同意する必要があるという述語は、上記の例buffer.isEmpty()にあります。そして、待機と通知がsynchronizedブロックで実行されるようにすることで、合意が解決されます。


この投稿は、記事としてここに書き直されました。 Java:同期ブロックで待機を呼び出す必要がある理由

264
aioobe

@Rollerballは正しい。 wait()が呼び出されると、このwait()呼び出しが発生したときにスレッドが何らかの条件が発生するのを待つことができるように、スレッドはロックを強制的に放棄します。
何かを放棄するには、まずそれを所有する必要があります。スレッドは最初にロックを所有する必要があります。したがって、synchronizedメソッド/ブロック内で呼び出す必要があります。

はい、synchronized method/block内で条件をチェックしなかった場合、潜在的な損害/不一致に関する上記のすべての回答に同意します。ただし、@ shrini1000が指摘しているように、同期ブロック内でwait()を呼び出しただけでは、この不一致が発生することは避けられません。

ここにニースの読み..

11

wait()の前にnotを同期すると、次のような問題が発生する可能性があります。

  1. 最初のスレッドがmakeChangeOnX()に入り、while条件をチェックし、それがtrueである場合(x.metCondition()falseを返します。つまり、x.conditionfalseを意味します)、その中に入ります。次に、wait()メソッドの直前に、別のスレッドがsetConditionToTrue()に移動し、x.conditiontrueおよびnotifyAll()に設定します。
  2. その後、最初のスレッドはwait()メソッドに入ります(数秒前に発生したnotifyAll()の影響を受けません)。この場合、1番目のスレッドは別のスレッドがsetConditionToTrue()を実行するのを待機しますが、再び発生することはありません。

しかし、オブジェクトの状態を変更するメソッドの前にsynchronizedを置いた場合、これは起こりません。

class A {

    private Object X;

    makeChangeOnX(){
        while (! x.getCondition()){
            wait();
            }
        // Do the change
    }

    setConditionToTrue(){
        x.condition = true; 
        notifyAll();

    }
    setConditionToFalse(){
        x.condition = false;
        notifyAll();
    }
    bool getCondition(){
        return x.condition;
    }
}
4
Shay.G

Wait()、notify()、およびnotifyAll()メソッドがスレッド間通信に使用されることは誰もが知っています。逃した信号と偽のウェイクアップ問題を取り除くために、待機スレッドは常にいくつかの条件で待機します。例えば。-

boolean wasNotified = false;
while(!wasNotified) {
    wait();
}

次に、通知スレッドはwasNotified変数をtrueに設定して通知します。

すべてのスレッドにはローカルキャッシュがあるため、すべての変更はまずそこに書き込まれ、その後徐々にメインメモリに昇格します。

これらのメソッドが同期ブロック内で呼び出されなかった場合、wasNotified変数はメインメモリにフラッシュされず、スレッドのローカルキャッシュに存在するため、通知スレッドによってリセットされたにもかかわらず、待機スレッドはシグナルを待機し続けます。

これらのタイプの問題を修正するために、これらのメソッドは常に同期ブロック内で呼び出され、同期ブロックが開始されると、すべてがメインメモリから読み取られ、同期ブロックを終了する前にメインメモリにフラッシュされます。

synchronized(monitor) {
    boolean wasNotified = false;
    while(!wasNotified) {
        wait();
    }
}

ありがとう、それが明確になることを願っています。

2
Aavesh Yadav

this Java Oracleチュートリアルから直接:

スレッドがd.waitを呼び出す場合、スレッドはdの組み込みロックを所有する必要があります。そうでない場合、エラーがスローされます。同期メソッド内で待機を呼び出すことは、組み込みロックを取得する簡単な方法です。

0
Rollerball

オブジェクトtからnotify()を呼び出すと、Javaは特定のt.wait()メソッドに通知します。しかし、Javaはどのようにして特定の待機メソッドを検索および通知しますか。

Javaは、オブジェクトtによってロックされたコードの同期ブロックのみを調べます。 Javaは、特定のt.wait()に通知するためにコード全体を検索できません。

0
lakshman reddy

これは基本的にハードウェアアーキテクチャに関連しています(つまりRAMおよびcaches)。

synchronizedwait()またはnotify()と一緒に使用しない場合、別のスレッドcouldは、モニターが入力するのを待つ代わりに、同じブロックに入ります。さらに、例えば同期ブロックなしで配列にアクセスすると、別のスレッドはその変更を認識しない可能性があります...実際には別のスレッドwill not変更を参照when既にコピーを持っていますCPUコアを処理するスレッドのxレベルキャッシュ(別名1st/2nd/3rdレベルキャッシュ)の配列。

ただし、同期ブロックはメダルの片側にすぎません。非同期コンテキストから同期コンテキスト内のオブジェクトに実際にアクセスする場合、オブジェクトは同期ブロック内でも同期されません。キャッシュ内のオブジェクト。私はこの問題についてここに書きました: https://stackoverflow.com/a/21462631 and ロックが最終でないオブジェクトを保持している場合、オブジェクトの参照は別のスレッドによってまだ変更できますか? ?

さらに、私はxレベルのキャッシュがほとんどの再現不可能なランタイムエラーの原因であると確信しています。開発者は通常、CPUの動作やメモリ階層がアプリケーションの実行にどのように影響するかなど、低レベルのものを学習しないためです。 http://en.wikipedia.org/wiki/Memory_hierarchy

プログラミングクラスが最初にメモリ階層とCPUアーキテクチャで開始しない理由は謎のままです。 「Hello world」はここでは役に立ちません。 ;)

0
Marcus

ドキュメントごと:

現在のスレッドがこのオブジェクトのモニターを所有している必要があります。スレッドは、このモニターの所有権を解放します。

wait()メソッドは、単にオブジェクトのロックを解除することを意味します。そのため、オブジェクトは同期ブロック/メソッド内でのみロックされます。スレッドが同期ブロックの外側にある場合、ロックされていないことを意味し、ロックされていない場合、オブジェクトで何を解放しますか?

0
Arun Raaj