Java的wait(), notify()和notifyAll()使用心得

投稿發表於2015-03-09

本篇文章是對java的 wait(),notify(),notifyAll()進行了詳細的分析介紹,需要的朋友參考下。

wait(),notify()和notifyAll()都是java.lang.Object的方法:
wait(): Causes the current thread to wait until another thread invokes the notify() method or the notifyAll() method for this object.
notify(): Wakes up a single thread that is waiting on this object’s monitor.
notifyAll(): Wakes up all threads that are waiting on this object’s monitor.

這三個方法,都是Java語言提供的實現執行緒間阻塞(Blocking)和控制程式內排程(inter-process communication)的底層機制。在解釋如何使用前,先說明一下兩點:

1. 正如Java內任何物件都能成為鎖(Lock)一樣,任何物件也都能成為條件佇列(Condition queue)。而這個物件裡的wait(), notify()和notifyAll()則是這個條件佇列的固有(intrinsic)的方法。

2. 一個物件的固有鎖和它的固有條件佇列是相關的,為了呼叫物件X內條件佇列的方法,你必須獲得物件X的鎖。這是因為等待狀態條件的機制和保證狀態連續性的機制是緊密的結合在一起的。

(An object’s intrinsic lock and its intrinsic condition queue are related: in order to call any of the condition queue methods on object X, you must hold the lock on X. This is because the mechanism for waiting for state-based conditions is necessarily tightly bound to the mechanism fo preserving state consistency)

根據上述兩點,在呼叫wait(), notify()或notifyAll()的時候,必須先獲得鎖,且狀態變數須由該鎖保護,而固有鎖物件與固有條件佇列物件又是同一個物件。也就是說,要在某個物件上執行wait,notify,先必須鎖定該物件,而對應的狀態變數也是由該物件鎖保護的。

知道怎麼使用後,我們來問下面的問題:

1. 執行wait, notify時,不獲得鎖會如何?

請看程式碼:

public static void main(String[] args) throws InterruptedException {
        Object obj = new Object();
        obj.wait();
        obj.notifyAll();
}

執行以上程式碼,會丟擲java.lang.IllegalMonitorStateException的異常。

2. 執行wait, notify時,不獲得該物件的鎖會如何?

請看程式碼:

public static void main(String[] args) throws InterruptedException {
        Object obj = new Object();
        Object lock = new Object();
        synchronized (lock) {
            obj.wait();
            obj.notifyAll();
        }
    }

執行程式碼,同樣會丟擲java.lang.IllegalMonitorStateException的異常。

3. 為什麼在執行wait, notify時,必須獲得該物件的鎖?

這是因為,如果沒有鎖,wait和notify有可能會產生競態條件(Race Condition)。考慮以下生產者和消費者的情景:

1.1生產者檢查條件(如快取滿了)-> 1.2生產者必須等待

2.1消費者消費了一個單位的快取 -> 2.2重新設定了條件(如快取沒滿) -> 2.3呼叫notifyAll()喚醒生產者

我們希望的順序是: 1.1->1.2->2.1->2.2->2.3

但在多執行緒情況下,順序有可能是 1.1->2.1->2.2->2.3->1.2。也就是說,在生產者還沒wait之前,消費者就已經notifyAll了,這樣的話,生產者會一直等下去。

所以,要解決這個問題,必須在wait和notifyAll的時候,獲得該物件的鎖,以保證同步。

請看以下利用wait,notify實現的一個生產者、一個消費者和一個單位的快取的簡單模型:

public class QueueBuffer {
    int n;
    boolean valueSet = false;
    synchronized int get() {
        if (!valueSet)
            try {
                wait();
            } catch (InterruptedException e) {
                System.out.println("InterruptedException caught");
            }
        System.out.println("Got: " + n);
        valueSet = false;
        notify();
        return n;
    }
    synchronized void put(int n) {
        if (valueSet)
            try {
                wait();
            } catch (InterruptedException e) {
                System.out.println("InterruptedException caught");
            }
        this.n = n;
        valueSet = true;
        System.out.println("Put: " + n);
        notify();
    }
}
public class Producer implements Runnable {
    private QueueBuffer q;
    Producer(QueueBuffer q) {
        this.q = q;
        new Thread(this, "Producer").start();
    }
    public void run() {
        int i = 0;
        while (true) {
            q.put(i++);
        }
    }
}
public class Consumer implements Runnable {
    private QueueBuffer q;
    Consumer(QueueBuffer q) {
        this.q = q;
        new Thread(this, "Consumer").start();
    }
    public void run() {
        while (true) {
            q.get();
        }
    }
}
public class Main {
    public static void main(String[] args) {
        QueueBuffer q = new QueueBuffer(); 
        new Producer(q); 
        new Consumer(q); 
        System.out.println("Press Control-C to stop."); 
    }
}

所以,JVM通過在執行的時候丟擲IllegalMonitorStateException的異常,來確保wait, notify時,獲得了物件的鎖,從而消除隱藏的Race Condition。

最後來看看一道題:寫一個多執行緒程式,交替輸出1,2,1,2,1,2……

利用wait, notify解決:

public class OutputThread implements Runnable {
    private int num;
    private Object lock;
    public OutputThread(int num, Object lock) {
        super();
        this.num = num;
        this.lock = lock;
    }
    public void run() {
        try {
            while(true){
                synchronized(lock){
                    lock.notifyAll();
                    lock.wait();
                    System.out.println(num);
                }
            }
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
    public static void main(String[] args){
        final Object lock = new Object();
        Thread thread1 = new Thread(new OutputThread(1,lock));
        Thread thread2 = new Thread(new OutputThread(2, lock));
        thread1.start();
        thread2.start();
    }
}

相關文章