在前篇介紹死鎖的文章中,咱們破壞等待佔用且等待條件時,用了一個死循環來獲取兩個帳本對象。html
// 一次性申請轉出帳戶和轉入帳戶,直到成功 while(!actr.apply(this, target)) ;
咱們提到過,若是apply()操做耗時很是短,且併發衝突量也不大,這種方案仍是能夠。不然的話,就可能要循環上萬次才能夠獲取鎖,這樣的話就太消耗CPU了!java
因而咱們給出另外一個更好的解決方案,等待-通知機制:
如果線程要求的條件不知足,則線程阻塞本身,進入等待狀態;當線程要求的條件知足時,通知等待的線程從新執行。編程
Java是支持這種等待-通知機制的,下面咱們就來詳細介紹這個機制,並用這個機制來優化咱們的轉帳流程。
咱們先經過一個就醫流程來了解一個完善的「等待-通知」機制。多線程
在醫院就醫的流程基本是以下這樣:併發
咱們將上述過程對應到線程的運行狀況:app
一個完整的「等待—通知」機制以下:
線程首先獲取互斥鎖,當線程要求條件不知足時,釋放互斥鎖,進入等待狀態;當條件知足時,通知等待的線程,從新獲取鎖。優化
必定要理解每個關鍵點,還須要注意,通知的時候雖然條件知足了,可是不表明該線程再次獲取到鎖時,條件仍是知足的。this
在Java中,等待—通知機制能夠有多種實現,這裏咱們講解由synchronized
配合wait()
、notify()
或者notifyAll()
的實現。spa
當線程進入獲取鎖進入同步代碼塊後,如果條件不知足,咱們便調用wait()
方法使得當前線程被阻塞且釋放鎖。線程
上圖中的等待隊列和互斥鎖是一一對應的,每一個互斥鎖都有本身的獨立的等待隊列(等待隊列是同一個)。(這句話還在暗示咱們後面喚醒線程時,是喚醒對應鎖上的線程。)
當條件知足時,咱們調用notify()
或者notifyAll()
,通知等待隊列(互斥鎖的等待隊列)中的線程,告訴它條件曾經知足過。
咱們要在相應的鎖上使用wait() 、notify()和notifyAll()。
須要注意,這三個方法能夠被調用的前提是咱們已經獲取到了相應的互斥鎖。因此,咱們會發現wait() 、notify() notifyAll()都是在synchronized{...}
內部中被調用的。若是在synchronized外部調用,JVM會拋出異常:java.lang.IllegalMonitorStateException。
咱們如今使用「等待—通知」機制來優化上篇的一直循環獲取鎖的方案。首先咱們要清楚以下以下四點:
使用「等待—通知」機制時,咱們通常會套用一個「範式」,能夠看做是前人的經驗總結用法。
while(條件不知足) { wait(); }
這個範式能夠解決「條件曾將知足過」這個問題。由於當wait()返回時,條件已經發生變化,使用這種結構就能夠檢驗條件是否還知足。
解決咱們的轉帳問題:
class Allocator { private List<Object> als; // 一次性申請全部資源 synchronized void apply(Object from, Object to){ // 經典寫法 while(als.contains(from) || als.contains(to)){ // from 或者 to帳戶被其餘線程擁有 try{ wait(); // 條件不知足時阻塞當前線程 }catch(Exception e){ } } als.add(from); als.add(to); } // 歸還資源 synchronized void free( Object from, Object to){ als.remove(from); als.remove(to); notifyAll(); // 歸還資源,喚醒其餘全部線程 } }
sleep()
和wait()
均可以使線程阻塞,可是它們仍是有很大的區別:
wait()、notify()以及notifyAll()它們之間的聯繫是依靠互斥鎖,也就同步鎖(內置鎖),咱們前面介紹過,每一個Java對象均可以用做一個實現同步的鎖,因此這些方法是定義在Object中,而不是Thread中。
「等待—通知」機制是一種很是廣泛的線程間協做的方式,咱們在理解時能夠利用生活中的例子去相似,就如上面的就醫流程。上文中沒有明顯說明notify()和notifyAll()的區別,只是在圖中標註了一下。咱們建議儘可能使用notifyAll(),notify() 是會隨機地通知等待隊列中的一個線程,在極端狀況下可能會使某個線程一直處於阻塞狀態不能去競爭獲取鎖致使線程「飢餓」;而 notifyAll() 會通知等待隊列中的全部線程,即全部等待的線程都有機會去獲取鎖的使用權。
參考: [1]極客時間專欄王寶令《Java併發編程實戰》 [2]Brian Goetz.Tim Peierls. et al.Java併發編程實戰[M].北京:機械工業出版社,2016 [3]skywang12345.Java多線程系列--「基礎篇」05之 線程等待與喚醒.https://www.cnblogs.com/skywang12345/p/3479224.html