根據網友的意見,修改版如下:
當你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;
}
首先
在上面生產者-消費者這個例子中,看起來我們用notify也能夠僥倖成功,因爲等待循環的哨兵對於消費者和生產者來說是互斥的。我們不能同時在put方法和get方法都有一個線程wait,如果這種情況允許的話,那麼下面的事情就會發生:
buf.size() == 0 AND buf.size() == MAX_SIZE (假設MAX_SIZE不爲0)
假設 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了,三個線程就會處於死鎖狀態。 |
在執行第五步時,即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時間片。