1. 程式人生 > >notify和notifyAll有什麼區別?

notify和notifyAll有什麼區別?

先說兩個概念:鎖池和等待池

鎖池:假設執行緒A已經擁有了某個物件(注意:不是類)的鎖,而其它的執行緒想要呼叫這個物件的某個synchronized方法(或者synchronized塊),由於這些執行緒在進入物件的synchronized方法之前必須先獲得該物件的鎖的擁有權,但是該物件的鎖目前正被執行緒A擁有,所以這些執行緒就進入了該物件的鎖池中。
等待池:假設一個執行緒A呼叫了某個物件的wait()方法,執行緒A就會釋放該物件的鎖後,進入到了該物件的等待池中

在java中,每個物件都有兩個池,鎖(monitor)池和等待池

 

wait() ,notifyAll(),notify() 三個方法都是Object類中的方法.

 

鎖池:假設執行緒A已經擁有了某個物件(注意:不是類)的鎖,而其它的執行緒想要呼叫這個物件的某個synchronized方法(或者synchronized塊),由於這些執行緒在進入物件的synchronized方法之前必須先獲得該物件的鎖的擁有權,但是該物件的鎖目前正被執行緒A擁有,所以這些執行緒就進入了該物件的鎖池中。

 

等待池:假設一個執行緒A呼叫了某個物件的wait()方法,執行緒A就會釋放該物件的鎖(因為wait()方法必須出現在synchronized中,這樣自然在執行wait()方法之前執行緒A就已經擁有了該物件的鎖),同時執行緒A就進入到了該物件的等待池中。如果另外的一個執行緒呼叫了相同物件的notifyAll()方法,那麼處於該物件的等待池中的執行緒就會全部進入該物件的鎖池中,準備爭奪鎖的擁有權。如果另外的一個執行緒呼叫了相同物件的notify()方法,那麼僅僅有一個處於該物件的等待池中的執行緒(隨機)會進入該物件的鎖池.
 

 ----------------------------------------------------------------------------分割線-----------------------------------------------------------------------------------------------------
然後再來說notify和notifyAll的區別

如果執行緒呼叫了物件的 wait()方法,那麼執行緒便會處於該物件的等待池中,等待池中的執行緒不會去競爭該物件的鎖。
當有執行緒呼叫了物件的 notifyAll()方法(喚醒所有 wait 執行緒)或 notify()方法(只隨機喚醒一個 wait 執行緒),被喚醒的的執行緒便會進入該物件的鎖池中,鎖池中的執行緒會去競爭該物件鎖。也就是說,呼叫了notify後只要一個執行緒會由等待池進入鎖池,而notifyAll會將該物件等待池內的所有執行緒移動到鎖池中,等待鎖競爭
優先順序高的執行緒競爭到物件鎖的概率大,假若某執行緒沒有競爭到該物件鎖,它還會留在鎖池中,唯有執行緒再次呼叫 wait()方法,它才會重新回到等待池中。而競爭到物件鎖的執行緒則繼續往下執行,直到執行完了 synchronized 程式碼塊,它會釋放掉該物件鎖,這時鎖池中的執行緒會繼續競爭該物件鎖。
Reference:執行緒間協作:wait、notify、notifyAll
綜上,所謂喚醒執行緒,另一種解釋可以說是將執行緒由等待池移動到鎖池,notifyAll呼叫後,會將全部執行緒由等待池移到鎖池,然後參與鎖的競爭,競爭成功則繼續執行,如果不成功則留在鎖池等待鎖被釋放後再次參與競爭。而notify只會喚醒一個執行緒。

有了這些理論基礎,後面的notify可能會導致死鎖,而notifyAll則不會的例子也就好解釋了

 1 public class WaitAndNotify {
 2 public static void main(String[] args) {
 3         Object co = new Object();
 4         System.out.println(co);
 5  
 6         for (int i = 0; i < 5; i++) {
 7             MyThread t = new MyThread("Thread" + i, co);
 8             t.start();
 9         }
10  
11 try {
12             TimeUnit.SECONDS.sleep(2);
13             System.out.println("-----Main Thread notify-----");
14             synchronized (co) {
15                 co.notify();
16             }
17  
18             TimeUnit.SECONDS.sleep(2);
19             System.out.println("Main Thread is end.");
20  
21         } catch (InterruptedException e) {
22             e.printStackTrace();
23         }
24     }
25  
26 static class MyThread extends Thread {
27 private String name;
28         private Object co;
29  
30         public MyThread(String name, Object o) {
31 this.name = name;
32             this.co = o;
33         }
34  
35 @Override
36         public void run() {
37             System.out.println(name + " is waiting.");
38             try {
39 synchronized (co) {
40 co.wait();
41                 }
42                 System.out.println(name + " has been notified.");
43             } catch (InterruptedException e) {
44                 e.printStackTrace();
45             }
46         }
47     }
48 }
49  
50  

 

 

執行結果:
[email protected]
Thread1 is waiting.
Thread2 is waiting.
Thread0 is waiting.
Thread3 is waiting.
Thread4 is waiting.
-----Main Thread notify-----
Thread1 has been notified.
Main Thread is end.

將其中的那個notify換成notifyAll,執行結果:
Thread0 is waiting.
Thread1 is waiting.
Thread2 is waiting.
Thread3 is waiting.
Thread4 is waiting.
-----Main Thread notifyAll-----
Thread4 has been notified.
Thread2 has been notified.
Thread1 has been notified.
Thread3 has been notified.
Thread0 has been notified.
Main Thread is end.

執行環境jdk8,結論:
notify喚醒一個等待的執行緒;notifyAll喚醒所有等待的執行緒

如何在 Java 中正確使用 wait, notify 和 notifyAll – 以生產者消費者模型為例
wait, notify 和 notifyAll,這些在多執行緒中被經常用到的保留關鍵字,在實際開發的時候很多時候卻並沒有被大家重視。本文對這些關鍵字的使用進行了描述。

在 Java 中可以用 wait、notify 和 notifyAll 來實現執行緒間的通訊。。舉個例子,如果你的Java程式中有兩個執行緒——即生產者和消費者,那麼生產者可以通知消費者,讓消費者開始消耗資料,因為佇列緩衝區中有內容待消費(不為空)。相應的,消費者可以通知生產者可以開始生成更多的資料,因為當它消耗掉某些資料後緩衝區不再為滿。

我們可以利用wait()來讓一個執行緒在某些條件下暫停執行。例如,在生產者消費者模型中,生產者執行緒在緩衝區為滿的時候,消費者在緩衝區為空的時候,都應該暫停執行。如果某些執行緒在等待某些條件觸發,那當那些條件為真時,你可以用 notify 和 notifyAll 來通知那些等待中的執行緒重新開始執行。不同之處在於,notify 僅僅通知一個執行緒,並且我們不知道哪個執行緒會收到通知,然而 notifyAll 會通知所有等待中的執行緒。換言之,如果只有一個執行緒在等待一個訊號燈,notify和notifyAll都會通知到這個執行緒。但如果多個執行緒在等待這個訊號燈,那麼notify只會通知到其中一個,而其它執行緒並不會收到任何通知,而notifyAll會喚醒所有等待中的執行緒。

在這篇文章中你將會學到如何使用 wait、notify 和 notifyAll 來實現執行緒間的通訊,從而解決生產者消費者問題。如果你想要更深入地學習Java中的多執行緒同步問題,我強烈推薦閱讀Brian Goetz所著的《Java Concurrency in Practice | Java 併發實踐》,不讀這本書你的 Java 多執行緒征程就不完整哦!這是我最向Java開發者推薦的書之一。

如何使用Wait
儘管關於wait和notify的概念很基礎,它們也都是Object類的函式,但用它們來寫程式碼卻並不簡單。如果你在面試中讓應聘者來手寫程式碼,用wait和notify解決生產者消費者問題,我幾乎可以肯定他們中的大多數都會無所適從或者犯下一些錯誤,例如在錯誤的地方使用 synchronized 關鍵詞,沒有對正確的物件使用wait,或者沒有遵循規範的程式碼方法。說實話,這個問題對於不常使用它們的程式設計師來說確實令人感覺比較頭疼。

第一個問題就是,我們怎麼在程式碼裡使用wait()呢?因為wait()並不是Thread類下的函式,我們並不能使用Thread.call()。事實上很多Java程式設計師都喜歡這麼寫,因為它們習慣了使用Thread.sleep(),所以他們會試圖使用wait() 來達成相同的目的,但很快他們就會發現這並不能順利解決問題。正確的方法是對在多執行緒間共享的那個Object來使用wait。在生產者消費者問題中,這個共享的Object就是那個緩衝區佇列。

第二個問題是,既然我們應該在synchronized的函式或是物件裡呼叫wait,那哪個物件應該被synchronized呢?答案是,那個你希望上鎖的物件就應該被synchronized,即那個在多個執行緒間被共享的物件。在生產者消費者問題中,應該被synchronized的就是那個緩衝區佇列。(我覺得這裡是英文原文有問題……本來那個句末就不應該是問號不然不太通……)

 

永遠在迴圈(loop)裡呼叫 wait 和 notify,不是在 If 語句
現在你知道wait應該永遠在被synchronized的背景下和那個被多執行緒共享的物件上呼叫,下一個一定要記住的問題就是,你應該永遠在while迴圈,而不是if語句中呼叫wait。因為執行緒是在某些條件下等待的——在我們的例子裡,即“如果緩衝區佇列是滿的話,那麼生產者執行緒應該等待”,你可能直覺就會寫一個if語句。但if語句存在一些微妙的小問題,導致即使條件沒被滿足,你的執行緒你也有可能被錯誤地喚醒。所以如果你不線上程被喚醒後再次使用while迴圈檢查喚醒條件是否被滿足,你的程式就有可能會出錯——例如在緩衝區為滿的時候生產者繼續生成資料,或者緩衝區為空的時候消費者開始小號資料。所以記住,永遠在while迴圈而不是if語句中使用wait!我會推薦閱讀《Effective Java》,這是關於如何正確使用wait和notify的最好的參考資料。

基於以上認知,下面這個是使用wait和notify函式的規範程式碼模板:

1
2
3
4
5
6
7
8
// The standard idiom for calling the wait method in Java
synchronized (sharedObject) {
    while (condition) {
    sharedObject.wait();
        // (Releases lock, and reacquires on wakeup)
    }
    // do action based upon condition e.g. take or put into queue
}
就像我之前說的一樣,在while迴圈裡使用wait的目的,是線上程被喚醒的前後都持續檢查條件是否被滿足。如果條件並未改變,wait被呼叫之前notify的喚醒通知就來了,那麼這個執行緒並不能保證被喚醒,有可能會導致死鎖問題。

Java wait(), notify(), notifyAll() 範例
下面我們提供一個使用wait和notify的範例程式。在這個程式裡,我們使用了上文所述的一些程式碼規範。我們有兩個執行緒,分別名為PRODUCER(生產者)和CONSUMER(消費者),他們分別繼承了了Producer和Consumer類,而Producer和Consumer都繼承了Thread類。Producer和Consumer想要實現的程式碼邏輯都在run()函式內。Main執行緒開始了生產者和消費者執行緒,並聲明瞭一個LinkedList作為緩衝區佇列(在Java中,LinkedList實現了佇列的介面)。生產者在無限迴圈中持續往LinkedList裡插入隨機整數直到LinkedList滿。我們在while(queue.size == maxSize)迴圈語句中檢查這個條件。請注意到我們在做這個檢查條件之前已經在佇列物件上使用了synchronized關鍵詞,因而其它執行緒不能在我們檢查條件時改變這個佇列。如果佇列滿了,那麼PRODUCER執行緒會在CONSUMER執行緒消耗掉佇列裡的任意一個整數,並用notify來通知PRODUCER執行緒之前持續等待。在我們的例子中,wait和notify都是使用在同一個共享物件上的。

 

import java.util.LinkedList;
import java.util.Queue;
import java.util.Random;
/**
* Simple Java program to demonstrate How to use wait, notify and notifyAll()
* method in Java by solving producer consumer problem.
*
*/
public class ProducerConsumerInJava {
    public static void main(String args[]) {
        System.out.println("How to use wait and notify method in Java");
        System.out.println("Solving Producer Consumper Problem");
        Queue<Integer> buffer = new LinkedList<>();
        int maxSize = 10;
        Thread producer = new Producer(buffer, maxSize, "PRODUCER");
        Thread consumer = new Consumer(buffer, maxSize, "CONSUMER");
        producer.start(); consumer.start(); }
    }
    /**
    * Producer Thread will keep producing values for Consumer
    * to consumer. It will use wait() method when Queue is full
    * and use notify() method to send notification to Consumer
    * Thread.
    *
    * @author WINDOWS 8
    *
    */
    class Producer extends Thread
    { private Queue<Integer> queue;
        private int maxSize;
        public Producer(Queue<Integer> queue, int maxSize, String name){
            super(name); this.queue = queue; this.maxSize = maxSize;
        }
        @Override public void run()
        {
            while (true)
                {
                    synchronized (queue) {
                        while (queue.size() == maxSize) {
                            try {
                                System.out .println("Queue is full, " + "Producer thread waiting for " + "consumer to take something from queue");
                                queue.wait();
                            } catch (Exception ex) {
                                ex.printStackTrace(); }
                            }
                            Random random = new Random();
                            int i = random.nextInt();
                            System.out.println("Producing value : " + i); queue.add(i); queue.notifyAll();
                        }
                    }
                }
            }
    /**
    * Consumer Thread will consumer values form shared queue.
    * It will also use wait() method to wait if queue is
    * empty. It will also use notify method to send
    * notification to producer thread after consuming values
    * from queue.
    *
    * @author WINDOWS 8
    *
    */
    class Consumer extends Thread {
        private Queue<Integer> queue;
        private int maxSize;
        public Consumer(Queue<Integer> queue, int maxSize, String name){
            super(name);
            this.queue = queue;
            this.maxSize = maxSize;
        }
        @Override public void run() {
            while (true) {
                synchronized (queue) {
                    while (queue.isEmpty()) {
                        System.out.println("Queue is empty," + "Consumer thread is waiting" + " for producer thread to put something in queue");
                        try {
                            queue.wait();
                        } catch (Exception ex) {
                            ex.printStackTrace();
                        }
                    }
                    System.out.println("Consuming value : " + queue.remove()); queue.notifyAll();
                }
            }
        }
    }

  

為了更好地理解這個程式,我建議你在debug模式裡跑這個程式。一旦你在debug模式下啟動程式,它會停止在PRODUCER或者CONSUMER執行緒上,取決於哪個執行緒佔據了CPU。因為兩個執行緒都有wait()的條件,它們一定會停止,然後你就可以跑這個程式然後看發生什麼了(很有可能它就會輸出我們以上展示的內容)。你也可以使用Eclipse裡的Step into和Step over按鈕來更好地理解多執行緒間發生的事情。

本文重點:
1. 你可以使用wait和notify函式來實現執行緒間通訊。你可以用它們來實現多執行緒(>3)之間的通訊。

2. 永遠在synchronized的函式或物件裡使用wait、notify和notifyAll,不然Java虛擬機器會生成 IllegalMonitorStateException。

3. 永遠在while迴圈裡而不是if語句下使用wait。這樣,迴圈會線上程睡眠前後都檢查wait的條件,並在條件實際上並未改變的情況下處理喚醒通知。

4. 永遠在多執行緒間共享的物件(在生產者消費者模型裡即緩衝區佇列)上使用wait。

5. 基於前文提及的理由,更傾向用 notifyAll(),而不是 notify()。

 

這是關於Java裡如何使用wait, notify和notifyAll的所有重點啦。你應該只在你知道自己要做什麼的情況下使用這些函式,不然Java裡還有很多其它的用來解決同步問題的方案。例如,如果你想使用生產者消費者模型的話,你也可以使用BlockingQueue,它會幫你處理所有的執行緒安全問題和流程控制。如果你想要某一個執行緒等待另一個執行緒做出反饋再繼續執行,你也可以使用CycliBarrier或者CountDownLatch。如果你只是想保護某一個資源的話,你也可以使用Semaphore。