你真的懂wait、notify和notifyAll嗎

生產者消費者模型是咱們學習多線程知識的一個經典案例,一個典型的生產者消費者模型以下:java

public void produce() { synchronized (this) { while (mBuf.isFull()) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } mBuf.add(); notifyAll(); } } public void consume() { synchronized (this) { while (mBuf.isEmpty()) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } mBuf.remove(); notifyAll(); } } 

這段代碼很容易引伸出來兩個問題:一個是wait()方法外面爲何是while循環而不是if判斷,另外一個是結尾處的爲何要用notifyAll()方法,用notify()行嗎。多線程

不少人在回答第二個問題的時候會想固然的說notify()是喚醒一個線程,notifyAll()是喚醒所有線程,可是喚醒而後呢,不論是notify()仍是notifyAll(),最終拿到鎖的只會有一個線程,那它們到底有什麼區別呢?ide

其實這是一個對象內部鎖的調度問題,要回答這兩個問題,首先咱們要明白java中對象鎖的模型,JVM會爲一個使用內部鎖(synchronized)的對象維護兩個集合,Entry SetWait Set,也有人翻譯爲鎖池和等待池,意思基本一致。學習

對於Entry Set:若是線程A已經持有了對象鎖,此時若是有其餘線程也想得到該對象鎖的話,它只能進入Entry Set,而且處於線程的BLOCKED狀態。測試

對於Wait Set:若是線程A調用了wait()方法,那麼線程A會釋放該對象的鎖,進入到Wait Set,而且處於線程的WAITING狀態。this

還有須要注意的是,某個線程B想要得到對象鎖,通常狀況下有兩個先決條件,一是對象鎖已經被釋放了(如曾經持有鎖的前任線程A執行完了synchronized代碼塊或者調用了wait()方法等等),二是線程B已處於RUNNABLE狀態。spa

那麼這兩類集合中的線程都是在什麼條件下能夠轉變爲RUNNABLE呢?線程

對於Entry Set中的線程,當對象鎖被釋放的時候,JVM會喚醒處於Entry Set中的某一個線程,這個線程的狀態就從BLOCKED轉變爲RUNNABLE。翻譯

對於Wait Set中的線程,當對象的notify()方法被調用時,JVM會喚醒處於Wait Set中的某一個線程,這個線程的狀態就從WAITING轉變爲RUNNABLE;或者當notifyAll()方法被調用時,Wait Set中的所有線程會轉變爲RUNNABLE狀態。全部Wait Set中被喚醒的線程會被轉移到Entry Set中。code

而後,每當對象的鎖被釋放後,那些全部處於RUNNABLE狀態的線程會共同去競爭獲取對象的鎖,最終會有一個線程(具體哪個取決於JVM實現,隊列裏的第一個?隨機的一個?)真正獲取到對象的鎖,而其餘競爭失敗的線程繼續在Entry Set中等待下一次機會。

有了這些知識點做爲基礎,上述的兩個問題就能解釋的清了。

首先來看第一個問題,咱們在調用wait()方法的時候,內心想的確定是由於當前方法不知足咱們指定的條件,所以執行這個方法的線程須要等待直到其餘線程改變了這個條件而且作出了通知。那麼爲何要把wait()方法放在循環而不是if判斷裏呢,其實答案顯而易見,由於wait()的線程永遠不能肯定其餘線程會在什麼狀態下notify(),因此必須在被喚醒、搶佔到鎖而且從wait()方法退出的時候再次進行指定條件的判斷,以決定是知足條件往下執行呢仍是不知足條件再次wait()呢。

就像在本例中,若是隻有一個生產者線程,一個消費者線程,那實際上是能夠用if代替while的,由於線程調度的行爲是開發者能夠預測的,生產者線程只有可能被消費者線程喚醒,反之亦然,所以被喚醒時條件始終知足,程序不會出錯。可是這種狀況只是多線程狀況下極爲簡單的一種,更廣泛的是多個線程生產,多個線程消費,那麼就極有可能出現喚醒生產者的是另外一個生產者或者喚醒消費者的是另外一個消費者,這樣的狀況下用if就必然會現相似過分生產或者過分消費的狀況了,典型如IndexOutOfBoundsException的異常。因此全部的java書籍都會建議開發者永遠都要把wait()放到循環語句裏面

而後來看第二個問題,既然notify()和notifyAll()最終的結果都是隻有一個線程能拿到鎖,那喚醒一個和喚醒多個有什麼區別呢?

耐心看下面這個兩個生產者兩個消費者的場景,若是咱們代碼中使用了notify()而非notifyAll(),假設消費者線程1拿到了鎖,判斷buffer爲空,那麼wait(),釋放鎖;而後消費者2拿到了鎖,一樣buffer爲空,wait(),也就是說此時Wait Set中有兩個線程;而後生產者1拿到鎖,生產,buffer滿,notify()了,那麼可能消費者1被喚醒了,可是此時還有另外一個線程生產者2在Entry Set中盼望着鎖,而且最終搶佔到了鎖,但由於此時buffer是滿的,所以它要wait();而後消費者1拿到了鎖,消費,notify();這時就有問題了,此時生產者2和消費者2都在Wait Set中,buffer爲空,若是喚醒生產者2,沒毛病;但若是喚醒了消費者2,由於buffer爲空,它會再次wait(),這就尷尬了,萬一輩子產者1已經退出再也不生產了,沒有其餘線程在競爭鎖了,只有生產者2和消費者2在Wait Set中互相等待,那傳說中的死鎖就發生了。

但若是你把上述例子中的notify()換成notifyAll(),這樣的狀況就不會再出現了,由於每次notifyAll()都會使其餘等待的線程從Wait Set進入Entry Set,從而有機會得到鎖。

其實說了這麼多,一句話解釋就是之因此咱們應該儘可能使用notifyAll()的緣由就是,notify()很是容易致使死鎖。固然notifyAll並不必定都是優勢,畢竟一次性將Wait Set中的線程都喚醒是一筆不菲的開銷,若是你能handle你的線程調度,那麼使用notify()也是有好處的。

最後我把完整的測試代碼放出來,供你們參考:

import java.util.ArrayList; import java.util.List; public class Something { private Buffer mBuf = new Buffer(); public void produce() { synchronized (this) { while (mBuf.isFull()) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } mBuf.add(); notifyAll(); } } public void consume() { synchronized (this) { while (mBuf.isEmpty()) { try { wait(); } catch (InterruptedException e) { e.printStackTrace(); } } mBuf.remove(); notifyAll(); } } private class Buffer { private static final int MAX_CAPACITY = 1; private List innerList = new ArrayList<>(MAX_CAPACITY); void add() { if (isFull()) { throw new IndexOutOfBoundsException(); } else { innerList.add(new Object()); } System.out.println(Thread.currentThread().toString() + " add"); } void remove() { if (isEmpty()) { throw new IndexOutOfBoundsException(); } else { innerList.remove(MAX_CAPACITY - 1); } System.out.println(Thread.currentThread().toString() + " remove"); } boolean isEmpty() { return innerList.isEmpty(); } boolean isFull() { return innerList.size() == MAX_CAPACITY; } } public static void main(String[] args) { Something sth = new Something(); Runnable runProduce = new Runnable() { int count = 4; @Override public void run() { while (count-- > 0) { sth.produce(); } } }; Runnable runConsume = new Runnable() { int count = 4; @Override public void run() { while (count-- > 0) { sth.consume(); } } }; for (int i = 0; i < 2; i++) { new Thread(runConsume).start(); } for (int i = 0; i < 2; i++) { new Thread(runProduce).start(); } } } 
  • 上面的栗子是正確的使用方式,輸出的結果以下:
Thread[Thread-2,5,main] add Thread[Thread-1,5,main] remove Thread[Thread-3,5,main] add Thread[Thread-0,5,main] remove Thread[Thread-3,5,main] add Thread[Thread-0,5,main] remove Thread[Thread-2,5,main] add Thread[Thread-1,5,main] remove Process finished with exit code 0 
  • 若是把while改爲if,結果以下,程序可能產生運行時異常:
Thread[Thread-2,5,main] add Thread[Thread-1,5,main] remove Thread[Thread-3,5,main] add Thread[Thread-1,5,main] remove Thread[Thread-3,5,main] add Thread[Thread-1,5,main] remove Exception in thread "Thread-0" Exception in thread "Thread-2" java.lang.IndexOutOfBoundsException at Something$Buffer.add(Something.java:42) at Something.produce(Something.java:16) at Something$1.run(Something.java:76) at java.lang.Thread.run(Thread.java:748) java.lang.IndexOutOfBoundsException at Something$Buffer.remove(Something.java:52) at Something.consume(Something.java:30) at Something$2.run(Something.java:86) at java.lang.Thread.run(Thread.java:748) Process finished with exit code 0 
  • 若是把notifyAll改成notify,結果以下,死鎖,程序沒有正常退出:
做者:A_客 連接:https://www.jianshu.com/p/25e243850bd2 來源:簡書 著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。
相關文章
相關標籤/搜索