なぜwait()は常に同期ブロックでなければならないのですか?


257

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

wait()同期されたブロックの外部で呼び出すことが可能で、そのセマンティクスを保持する-呼び出し元のスレッドを一時停止する可能性がある場合、どのような損害が考えられますか?

回答:


232

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

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

if(!condition){
    wait();
}

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

スレッドの待機が終了したからといって、探している状態が真であるとは限りません。

  • 偽のウェイクアップ(スレッドが通知を受け取らずに待機からウェイクアップできる)を取得できる、または

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

これらのケースに対処するために本当に必要なのは、常にこれのいくつかのバリエーションです。

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

さらに良いことに、同期プリミティブをまったく変更しないで、java.util.concurrentパッケージで提供されている抽象化を使用してください。


3
基本的に同じことを言って、ここにも詳細な議論があります。coding.derkeiler.com/Archive/Java/comp.lang.java.programmer/...

1
ところで、中断されたフラグを無視しない場合は、ループThread.interrupted()もチェックします。
bestsss

2
while(!condition){synchronized(this){wait();}}は、次のようなことを行うことができます。つまり、同期ブロックでwait()が正しく呼び出されても、条件のチェックと待機の間には競合があります。それで、おそらくJavaでの実装方法が原因で、この制限の背後に他の理由がありますか?
shrini1000

9
別の厄介なシナリオ:条件がfalseの場合、今度はwait()に入り、別のスレッドが条件を変更してnotify()を呼び出します。まだwait()を使用していないため、このnotify()はありません。つまり、テストと待機、および変更と通知はアトミックである必要があります。

1
@Nullpointer:アトミックに記述できるタイプ(if句で直接使用することで暗黙的に指定されるブール値など)で、他の共有データとの相互依存性がない場合、それを揮発性として宣言すれば回避できます。ただし、更新が他のスレッドからすぐに見えるようにするには、それまたは同期が必要です。
Michael Borgwardt 2017年

283

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()(そして欠場notify()だけ呼ばれていたことを)。

  4. 運が悪いと、プロデューサースレッドはgive()、コンシューマースレッドが起動しないため、デッドロックが発生するため、これ以上生成しません。

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

詳細には触れませんが、この同期の問題は一般的なものです。Michael Borgwardtが指摘しているように、待機/通知はすべてスレッド間の通信に関するものであるため、常に上記のような競合状態が発生します。これが、「同期内でのみ待機する」ルールが適用される理由です。


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

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

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


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


さらに、wait()が終了した直後に条件に加えられた変更が確実に反映されるようにするためにも、私は推測します。それ以外の場合は、notify()がすでに呼び出されているため、デッドロックも発生します。
Surya Wijaya Madjid 2012

興味深いですが、synchronizedを呼び出すだけでは、wait()とnotify()の「信頼性が低い」という性質により、必ずしもこのような問題が解決されるわけではありません。詳細はこちら:stackoverflow.com/questions/21439355/…。同期が必要な理由は、ハードウェアアーキテクチャ内にあります(以下の私の回答を参照してください)。
Marcus、

しかしreturn buffer.remove();、whileブロックを追加した後wait();、それが機能するのですか?
ボブジャン

@BobJiang、いいえ、誰かがギブを呼ぶ以外の理由でスレッドが起こされる可能性があります。つまり、wait復帰後もバッファが空になる場合があります。
aioobe

私はtry-catchで囲まれThread.currentThread().wait();main関数のみを持っていInterruptedExceptionます。synchronizedブロックなしでは、同じ例外が発生しIllegalMonitorStateExceptionます。現在、違法な状態になっているのはなぜですか?synchronizedただし、ブロック内で機能します。
Shashwat

12

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

はい、私はあなたがsynchronizedメソッド/ブロック内の状態をチェックしなかった場合の潜在的な損傷/不整合に関する上記のすべての回答に同意します。ただし、@ shrini1000が指摘したように、wait()同期ブロック内で呼び出すだけでは、この不整合の発生を回避できません。

これはいい読みです。


5
@Popeye「適切に」適切に説明します。あなたのコメントは誰にとっても役に立たない。
ローンの侯爵

4

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

  1. 最初のスレッドが入っmakeChangeOnX()てwhile条件をチェックし、それがtruex.metCondition()returns false、means x.conditionis false)である場合、その内部に入ります。そして、直前wait()の方法、別のスレッドがに行くsetConditionToTrue()とセットx.conditiontrueしてnotifyAll()
  2. その後、最初のスレッドが彼のwait()メソッドに入ります(notifyAll()直前に発生したものの影響を受けません)。この場合、最初のスレッドは別のスレッドの実行を待機し続けます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;
    }
}

2

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

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

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

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

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

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

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

よろしくお願いいたします。


1

これは基本的にハードウェアアーキテクチャ(RAMキャッシュなど)に関係しています。

あなたが使用していない場合はsynchronized一緒wait()notify()、別のスレッドは、可能性が代わりにそれを入力するモニターを待っているのと同じブロックを入力してください。例えば同期ブロックせずに配列にアクセスするときに、また、別のスレッドがそれにchangementが表示されないことがあります...実際には別のスレッドではないだろう、それに任意のchangementsを見たとき、それはすでにX-レベルのキャッシュに配列のコピーを持っています(別名1st / 2nd / 3rd-levelキャッシュ)スレッド処理CPUコアの。

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

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

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


1
:ちょうど完璧との綿密それを説明するウェブサイトを発見しjavamex.com/tutorials/...
マーカス

うーん..私がフォローするかどうかわからない。キャッシュが同期内で待機と通知を行う唯一の理由である場合、同期が待機/通知の実装内で行われないのはなぜですか?
aioobe 2015年

良い質問です。待機/通知は同期されたメソッドである可能性が高いので...おそらくSunの以前のJava開発者が答えを知っているでしょうか。上記のリンクを確認してください。そうでない場合は、これも役立つでしょう:docs.oracle.com/javase/specs/jls/se7/html/jls-17.html
Marcus

理由は次のとおりです。Javaの初期の頃には、これらのマルチスレッド操作を行う前に同期を呼び出さなかった場合のコンパイルエラーはありませんでした。代わりに、実行時エラーのみが発生しました(例:coderanch.com/t/239491/java-programmer-SCJP/certification/…)。おそらく彼らは@SUNがプログラマーがこれらのエラーを取得しているときに彼らが連絡を受けていると本当に思っていたのかもしれません。いつ変更されましたか?Java 5.0か6.0かもしれませんが、実際には正直に覚えていません...
Marcus

TBH回答にいくつかの問題があると思います1)2番目の文は意味がありません。スレッドがロックしているオブジェクトは関係ありません。2つのスレッドが同期するオブジェクトに関係なく、すべての変更が表示されます。2)別のスレッド変更をない」と言います。これは「そうではない」はずです。3)なぜ1、2、3レベルのキャッシュを起動するのかわかりません...ここで重要なのは、Javaメモリモデルの説明であり、JLSで指定されています。ハードウェアアーキテクチャは JLSが何をするのを理解するのに役立つ場合がありますが、この文脈では厳密には無関係です。
aioobe 2015年

0

この Java oracleチュートリアルから直接:

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


作者が出した質問から、チュートリアルから引用した内容を質問作者が明確に理解しているようには見えません。さらに、私の答えは、「なぜ」を説明しています。
ローラーボール2013年

0

オブジェクトtからnotify()を呼び出すと、javaは特定のt.wait()メソッドに通知します。しかし、Javaは特定の待機メソッドを検索して通知する方法を教えてください。

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


0

ドキュメントに従って:

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

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


0

監視オブジェクト(同期ブロックで使用されるオブジェクト)でのスレッド待機、単一スレッドの全過程でn個の監視オブジェクトが存在する可能性があります。スレッドが同期ブロックの外側で待機する場合、監視オブジェクトはなく、他のスレッドも監視オブジェクトへのアクセスを通知するので、同期ブロックの外側のスレッドは通知されたことをどのようにして知ることができますか。これは、wait()、notify()、notifyAll()がスレッドクラスではなくオブジェクトクラスにある理由の1つでもあります。

基本的に、監視オブジェクトはすべてのスレッドに共通のリソースであり、監視オブジェクトは同期ブロックでのみ使用できます。

class A {
   int a = 0;
  //something......
  public void add() {
   synchronization(this) {
      //this is your monitoring object and thread has to wait to gain lock on **this**
       }
  }
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.