java中的notify和notifyAll有什麼區別?

作者:知乎用戶
鏈接:https://www.zhihu.com/question/37601861/answer/145545371
來源:知乎
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

今天正好碰到這個問題,也疑惑了好久。看了一圈知乎上的答案,感覺沒說到根上。所以自己又好好Google了一下,終於找到了讓自己信服的解釋。

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

鎖池:假設線程A已經擁有了某個對象(注意:不是類)的鎖,而其它的線程想要調用這個對象的某個synchronized方法(或者synchronized塊),由於這些線程在進入對象的synchronized方法之前必須先獲得該對象的鎖的擁有權,但是該對象的鎖目前正被線程A擁有,所以這些線程就進入了該對象的鎖池中。
等待池:假設一個線程A調用了某個對象的wait()方法,線程A就會釋放該對象的鎖後,進入到了該對象的等待池中
Reference:java中的鎖池和等待池
然後再來說notify和notifyAll的區別

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

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

作者:Alex Wang
鏈接:https://www.zhihu.com/question/37601861/answer/94679949
來源:知乎
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

還是直接上代碼:
public class WaitAndNotify {
public static void main(String[] args) {
Object co = new Object();
System.out.println(co);

    for (int i = 0; i < 5; i++) {
        MyThread t = new MyThread("Thread" + i, co);
        t.start();
    }

try {
TimeUnit.SECONDS.sleep(2);
System.out.println("-----Main Thread notify-----");
synchronized (co) {
co.notify();
}

        TimeUnit.SECONDS.sleep(2);
        System.out.println("Main Thread is end.");

    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

static class MyThread extends Thread {
private String name;
private Object co;

    public MyThread(String name, Object o) {

this.name = name;
this.co = o;
}

@Override
public void run() {
System.out.println(name + " is waiting.");
try {
synchronized (co) {
co.wait();
}
System.out.println(name + " has been notified.");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

運行結果:br/>java.lang.Object@1540e19d
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.
  • @author Javin Paul
    */
    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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章