スレッド同期のwaitとnotify、notifyall原理

19990 ワード

スレッドが同期するときに一般的な方法はwaitとnotify/notifyallを組み合わせて使用することです.以下に示すように、この非常に古典的な方法は、同期プロセスに絶対的に安全です.
package com.company;

public class A {
    private boolean condition;
    private Object lock;

    public void work() {
        AThead thead = new AThead();
        thead.start();
        lock = new Object();

        synchronized (lock) {
            while (!condition) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.println("work");
    }

    public class AThead extends Thread {
        @Override
        public void run() {
            super.run();
            System.out.println("start");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock) {
                condition = true;
                lock.notifyAll();
            }
        }
    }
}


しかし、私はいくつかの疑問を持っています.1、waitの本当の意味は何ですか.2、waitとnotifyはどうして同期ブロックに入れて実行しますか?3、なぜwaitの時にwhileサイクル確認条件を満たす必要があるのですか?4、conditionはvolatileを追加する必要がありますか?
このいくつかの問題が、見官がはっきりしていれば、下を見る必要はありません.
wait
まずwaitに何の役に立つのでしょうか?以下は公式の解釈です
public final void wait(long timeout)
                throws InterruptedException
Causes the current thread to wait until either another thread invokes the notify() method or the notifyAll() method for this object, or a specified amount of time has elapsed.
The current thread must own this object's monitor.

This method causes the current thread (call it T) to place itself in the wait set for this object and then to relinquish any and all synchronization claims on this object. Thread T becomes disabled for thread scheduling purposes and lies dormant until one of four things happens:

Some other thread invokes the notify method for this object and thread T happens to be arbitrarily chosen as the thread to be awakened.
Some other thread invokes the notifyAll method for this object.
Some other thread interrupts thread T.
The specified amount of real time has elapsed, more or less. If timeout is zero, however, then real time is not taken into consideration and the thread simply waits until notified.
The thread T is then removed from the wait set for this object and re-enabled for thread scheduling. It then competes in the usual manner with other threads for the right to synchronize on the object; once it has gained control of the object, all its synchronization claims on the object are restored to the status quo ante - that is, to the situation as of the time that the wait method was invoked. Thread T then returns from the invocation of the wait method. Thus, on return from the wait method, the synchronization state of the object and of thread T is exactly as it was when the wait method was invoked.
A thread can also wake up without being notified, interrupted, or timing out, a so-called spurious wakeup. While this will rarely occur in practice, applications must guard against it by testing for the condition that should have caused the thread to be awakened, and continuing to wait if the condition is not satisfied. In other words, waits should always occur in loops, like this one:

     synchronized (obj) {
         while ()
             obj.wait(timeout);
         ... // Perform action appropriate to condition
     }
 
(For more information on this topic, see Section 3.2.3 in Doug Lea's "Concurrent Programming in Java (Second Edition)" (Addison-Wesley, 2000), or Item 50 in Joshua Bloch's "Effective Java Programming Language Guide" (Addison-Wesley, 2001).
If the current thread is interrupted by any thread before or while it is waiting, then an InterruptedException is thrown. This exception is not thrown until the lock status of this object has been restored as described above.

Note that the wait method, as it places the current thread into the wait set for this object, unlocks only this object; any other objects on which the current thread may be synchronized remain locked while the thread waits.

This method should only be called by a thread that is the owner of this object's monitor. See the notify method for a description of the ways in which a thread can become the owner of a monitor.

まとめると、waitは現在のスレッドをブロック状態にし、objectのロックを解放します.この方法は、notify/notifyAllがトリガーされたときまたはスレッドがinterruptsされた場合にのみ返されます.
なぜsynchronizeが必要なのか
waitとnotifyはsynchronizeブロックにある必要があります.同期を維持するには、なぜそうする必要がありますか.synchronizeブロックに入れないと、デッドロックが発生する可能性があります.例を挙げる
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();
    }
}

これはwait,notifyを用いて書かれたブロックキューであり、同期がなければ、このようなシーンAスレッドがtakeを呼び出し、bufferを発見する可能性がある.isEmpty true Bスレッド実行give Aスレッド実行wait
このときAスレッドはnotifyを待つことができなくなり、Bスレッドはwaitの前にnotifyを呼び出すので、Aスレッドは天荒地老まで待つしかありません.
なぜwhileサイクル確認条件を満たす必要があるのか
主な2つの理由1、他のスレッドがnotify/notifyAllを呼び出している可能性がありますが、現在の業務とは関係ないので条件判断をしなければなりません2、spurious wakeup(虚偽の起動)がある場合、まったく通知されていないかもしれませんし、スレッドも起動されています.これもよくわかりませんので参考にしてくださいhttps://stackoverflow.com/questions/1050592/do-spurious-wakeups-in-java-actually-happen
notifyを使うかnotifyallを使うか
待機しているスレッドを呼び覚ますには、notifyとnotifyallのどちらを使用しますか?本来、notifyは単一の待機中のスレッドを起動し、notifyallはすべての待機中のスレッドを起動する.一般的な言い方は、notifyallを使うべきだということです.これは合理的で保守的なアドバイスです.彼はいつも正しい結果を生み出すことができます.彼はあなたが呼び覚ます必要があるすべてのスレッドを呼び覚ますことを保証することができるからです.他のスレッドも呼び出されるかもしれませんが、プログラムの正確性には影響しません.これらのスレッドは目が覚めると、待機している条件をチェックし、条件が満たされていない場合は待機し続けます.
conditionはvolatileを追加する必要がありますか?
Synchronizedは原子性と可視性を実現することができる.Javaメモリモデルではsynchronizedで,スレッドはロック時にワークメモリを空にする→メインメモリに最新変数のコピーをワークメモリにコピーする→コードを実行する→変更後の共有変数の値をメインメモリにリフレッシュする→反発ロックを解放することを規定している.
もっと良い実現方法はありますか?
上記のソリューションは非常に古典的で、優雅で安定していますが、javaのconcurrentパッケージは実際には、CountDownLatch、Atomic、Semaphore、CyclicBarrier、ConcurrentHashMapなど、より高度な言語で同期方法を実現し、高次の方法でできるだけ高次のものを使用することができます.例えば、上記のコードでは、同期の問題を考慮することなく、CountDownLatchで解決することができます.
package com.company;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Semaphore;

public class B {
    private CountDownLatch countDownLatch;

    public void work() {
        countDownLatch = new CountDownLatch(1);
        AThead thead = new AThead();
        thead.start();

        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("work");
    }

    public class AThead extends Thread {
        @Override
        public void run() {
            super.run();
            System.out.println("start");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("end");
            countDownLatch.countDown();
        }
    }
}


ref
https://docs.oracle.com/javase/6/docs/api/java/lang/Object.html#wait(long) https://stackoverflow.com/questions/2779484/why-must-wait-always-be-in-synchronized-block https://stackoverflow.com/questions/1050592/do-spurious-wakeups-in-java-actually-happen java effective 69