notify和notifyAll的一段兒代碼分析

【原文來自ImportNew-宋濤

根據網友的意見,修改版如下:

當你Google”notify()和notifyAll()的區別”時,會有大片的結果彈出來,(這裏先把jdk的javadoc文檔那一段撇開不說),所有這些搜索結果歸結爲等待的線程被喚醒的數量:notify()是喚醒一個, 而notifyall()是喚醒全部.那他們的真正區別是什麼呢?

讓我們來看看個生產者/消費者的案例(假設生產者/消費者這個類中有兩個方法put和get),它是有問題的(因爲這裏用到了notify方法),是的,這段代碼也許會執行,甚至大部分情況下能夠正常運行,但是它也是有可能會出發生死鎖,我們來看一看原因:

      public synchronized void put(Object o) {  
          while (buf.size()==MAX_SIZE) {  
              wait(); // 如果buffer爲full,就會執行wait方法等待(爲了簡單,我們省略try/catch語句塊)  
          }  
          buf.add(o);  
          notify(); // 通知所有正在等待對象鎖的Producer和Consumer(譯者注:包括被阻擋在方法外的Producer和Consumer)  
     }  
        
     // Y: 這裏是C2試圖獲取鎖的地方(原作者將這個方法放到了get方法裏面,此處,我把它放在了方法的外面)  
     public synchronized Object get() {  
          while (buf.size()==0) {  
              wait(); // 如果buffer爲Null,就會執行wait方法(爲了簡單,同樣省略try/catch語句塊)  
              // X: 這裏是C1試圖重新獲得鎖的地方(看下面代碼)  
          }  
          Object o = buf.remove(0);  
          notify(); // 通知所有正在等待對象鎖的Producer和Consumer(譯者注:包括被阻擋在方法外的Producer和Consumer)  
          return o;  
     }


首先

我們爲什麼在wait方法外面加上while循環?
我們需要使用while循環來實現下面的情景,
情景分析:
消費者1(C1)進入同步塊中,此時buf是空的,所以C1被放入wait 隊列中(因爲執行了wait方法,譯者注:此時C2是恰好到方法處,而不是因爲有線程在方法中運行才被阻擋在方法外的),當消費者2(C2)正要進入同步方法的時候(此時在Y的上面),生產者P1將一個對象放入到buf中,隨後又調用notify方法。此時wait 隊列中唯一的線程是C1(譯者注:C2不在waiting 隊列中,也不在blocked隊列中),所以C1被喚醒,C1被喚醒之後又開始試圖重新獲得對象鎖,此時C1還在X的上面。
現在的情況是,C1和C2都在試圖去獲取同步鎖,這兩個線程只能有一個被選擇進入方法,另一個則會被堵塞(不是waiting,而是blocked 。譯者注:雖然C1已經在方法中,不過還是會和C2競爭鎖,如果C2獲得鎖,則C2進入方法執行接下來的操作,而C1還是繼續等待鎖(處於blocked狀態);如果C1獲得鎖,則C1往下執行,而C2還是會被擋在方法外面(處於blocked狀態))。假如C2先獲得了對象鎖,C1仍然被阻擋着(此時C1還試圖在X處獲得鎖),C2完成了方法,並釋放了鎖。現在C1獲得了鎖。假設這裏沒有while循環,那麼C1就會往下執行,從buf中刪除一個對象,但是此時buf中已經沒有對象了,因爲剛剛C2已經取走了一個對象,如果此時C1執行buf.remove(0),則會報IndexArrayOutOfBoundsException異常。爲了防止這樣的異常發生,我們在上面用到了while循環,在往下執行之前,判斷此時buf的大小是否爲0,如果不是0,則往下執行,如果是0,則繼續wait()。
那麼我們這裏引出問題:爲什麼需要notifyAll?

在上面生產者-消費者這個例子中,看起來我們用notify也能夠僥倖成功,因爲等待循環的哨兵對於消費者和生產者來說是互斥的。我們不能同時在put方法和get方法都有一個線程wait,如果這種情況允許的話,那麼下面的事情就會發生:

buf.size() == 0 AND buf.size() == MAX_SIZE (假設MAX_SIZE不爲0) 

然而,這樣並不好,我們需要使用notifyAll。讓我們來看一看原因:
假設 buffer=1(爲了更加容易理解),按照下面的步驟執行將會發生死鎖。要注意的是:notify可以喚醒任何一個線程,不過JVM不能確定是哪個線程被喚醒,所以,任何一個線程都有被喚醒的可能。另外要注意的是,當多個線程被阻塞在方法外的時候(在試圖獲得鎖),獲得鎖的順序也是不確定的。要記住,在任何時候,方法中只能有一個線程存在-在類中任何同步的方法只允許一個線程執行(這個線程要持有對象鎖纔可以執行)。如果下面的執行順序發生了的話,就會導致死鎖:

第一步:
P1放入一個對象到buffer中;
第二步:
P2試圖put一個對象,此時buf中已經有一個了,所以wait
第三步:
P3試圖put一個對象,仍然wait
第四步:
C1試圖從buf中獲得一個對象;
C2試圖從buf中獲得一個對象,但是擋在了get方法外面
C3試圖從buf中獲得一個對象,同樣擋在了get方法外面
第五步:
C1執行完get方法,執行notify,退出方法
notify喚醒了P2,
但是C2在P2喚醒之前先進入了get方法,所以P2必須再次獲得鎖,P2被擋在了put方法的外面,
C2循環檢查buf大小,在buf中沒有對象,所以只能wait;
C3在C2之後,P2之前進入了方法,由於buf中沒有對象,所以也wait;
第六步:
現在,有P3,C2,C3在waiting;
最後P2獲得了鎖,在buf中放入了一個對象,執行notify,退出put方法;
第七步:
notify喚醒P3;
P3檢查循環條件,在buf中已經有了一個對象,所以wait;
現在沒有線程能夠notify了,三個線程就會處於死鎖狀態。

那麼如果使用notifyAll方法喚醒線程,又會怎樣呢?
在執行第五步時,即C1執行完get方法後,又執行了notifyAll方法,此時,notifyAll方法會喚醒所有正在等待該鎖的線程,那麼所有的線程都會處於運行前的準備狀態(此時不是wait狀態),此時,即使C2在P2(此時P2已經被喚醒,P3也被喚醒,處於準備狀態,而不是wait狀態)之前先進入了get方法,C2循環檢查buf大小,在buf中沒有對象,所以進入wait狀態;C3在C2之後,P2之前進入方法,由於buf中沒有對象,所以也wait;(這裏重新分析了一下步驟五發生的情景)
第六步:現在,有C2,C3在waiting,P3在第五步已經被喚醒了,處於準備狀態,此時,如果P2獲得鎖,在buf中放入一個對象,執行notifyAll,又將C2、C3喚醒了;
第七步:此時,P3檢查循環條件,在buf中已經有了一個對象,所以wait,不過此時並不會發生死鎖,因爲C2和C3還會繼續執行。

總結:notify方法很容易引起死鎖,除非你根據自己的程序設計,確定不會發生死鎖,notifyAll方法則是線程的安全喚醒方法。

附:
notify和notifyAll的區別:
notify()和notifyAll()都是Object對象用於通知處在等待該對象的線程的方法。
void notify(): 喚醒一個正在等待該對象的線程。
void notifyAll(): 喚醒所有正在等待該對象的線程。

兩者的最大區別在於:
     notifyAll使所有原來在該對象上等待被notify的線程統統退出wait的狀態,變成等待該對象上的鎖,一旦該對象被解鎖,他們就會去競爭。
     notify他只是選擇一個wait狀態線程進行通知,並使它獲得該對象上的鎖,但不驚動其他同樣在等待被該對象notify的線程們,當第一個線程運行完畢以後釋放對象上的鎖,此時如果該對象沒有再次使用notify語句,即便該對象已經空閒,其他wait狀態等待的線程由於沒有得到該對象的通知,繼續處在wait狀態,直到這個對象發出一個notify或notifyAll,它們等待的是被notify或notifyAll,而不是鎖。

以前大夥看到這兩個區別的時候可能感覺到很懵,相信現在應該有些明白了吧,如果還沒有搞清楚可以看一下我這裏案例的分析:notify發生死鎖的情景分析

原文:http://stackoverflow.com/questions/37026/java-notify-vs-notifyall-all-over-again


根據網友的評論補充:

解釋下前三步:
synchronized修飾的方法,同一時刻只能允許一個線程進入,所以第一步執行之後,P1執行notify,跳出方法,然後,P2可以進入synchronized修飾的put方法,不過這一次是wait,P2線程會釋放對象鎖,此時P3就可以進入put方法,當然了,還是wait(),釋放了對象鎖。


第四步:
get方法也是用synchronized修飾的,所以同一時刻,只能有一個線程進入此方法,C1進入之後,試圖取走一個對象,但此時還沒有取走,此時,C2準備進入get方法,不過因爲這個方法是用synchronized修飾的,所以C2被擋在了方法的外面,同理,C3也被擋在了方法的外面。(注意:此時C1還沒有取走對象)。

第五步:
當C1取走對象後,在執行notify方法之前,P2,P3會繼續wait,繼續等着notify的通知。
C1執行notify方法,通知了P2,此時P2可以獲得對象鎖了(這裏的意思是說:P2可以去搶對象鎖了,但是能不能搶得到就看它的造化了)。(注意:此時的C2以及C3還在外面等着,不過它們不是因爲執行了wait而等,所以它們不需要等notify的通知,只要有對象鎖,它們兩個就可以爭搶,獲得了對象鎖的就可以進入方法,所以,雖然notify通知了P2,但是C2和P2同屬競爭關係,所以C2是可以在P2之前獲得對象鎖的)。


網友補充:

C2,C3在C1進入get方法後會被jvm放入對象的鎖池中,而P2,P3是被放入對象的等待池中,等待池的線程只有通過notify、notifyAll或者interrupt才能進入鎖池中,而鎖池的線程只有拿到鎖標識後才進入runnable狀態等待cpu時間片。


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