【Java併發基礎】使用「等待—通知」機制優化死鎖中佔用且等待解決方案

前言

在前篇介紹死鎖的文章中,咱們破壞等待佔用且等待條件時,用了一個死循環來獲取兩個帳本對象。html

// 一次性申請轉出帳戶和轉入帳戶,直到成功
while(!actr.apply(this, target))
  ;

咱們提到過,若是apply()操做耗時很是短,且併發衝突量也不大,這種方案仍是能夠。不然的話,就可能要循環上萬次才能夠獲取鎖,這樣的話就太消耗CPU了!java

因而咱們給出另外一個更好的解決方案,等待-通知機制
如果線程要求的條件不知足,則線程阻塞本身,進入等待狀態;當線程要求的條件知足時,通知等待的線程從新執行。編程

Java是支持這種等待-通知機制的,下面咱們就來詳細介紹這個機制,並用這個機制來優化咱們的轉帳流程。
咱們先經過一個就醫流程來了解一個完善的「等待-通知」機制。多線程

就醫流程—完整的「等待—通知」機制

在醫院就醫的流程基本是以下這樣:併發

  1. 患者先去掛號,而後到就診門口分診,等待叫號;
  2. 當叫到本身的號時,患者就能夠找醫生就診;
  3. 就診過程當中,醫生可能會讓患者去作檢查,同時叫一位患者;
  4. 當患者作完檢查後,拿着檢查單從新分診,等待叫號;
  5. 當醫生再次叫到本身時,患者就再去找醫生就診。

咱們將上述過程對應到線程的運行狀況:app

  1. 患者到就診門口分診,相似於線程要去獲取互斥鎖;
  2. 當患者被叫到號時,相似於線程獲取到了鎖;
  3. 醫生讓患者去作檢查(缺少檢查報告不能診斷病因),相似於線程要求的條件沒有知足;
    患者去作檢查,相似於線程進入了等待狀態;而後醫生叫下一個患者,意味着線程釋放了持有的互斥鎖;
  4. 患者作完檢查,相似於線程要求的條件已經知足;患者拿着檢查報告從新分診,相似於線程須要從新獲取互斥鎖。

一個完整的「等待—通知」機制以下:
線程首先獲取互斥鎖,當線程要求條件不知足時,釋放互斥鎖,進入等待狀態;當條件知足時,通知等待的線程,從新獲取鎖優化

必定要理解每個關鍵點,還須要注意,通知的時候雖然條件知足了,可是不表明該線程再次獲取到鎖時,條件仍是知足的。this

Java中「等待—通知」機制的實現

在Java中,等待—通知機制能夠有多種實現,這裏咱們講解由synchronized配合wait()notify()或者notifyAll()的實現。spa

如何使線程等待,wait()

當線程進入獲取鎖進入同步代碼塊後,如果條件不知足,咱們便調用wait()方法使得當前線程被阻塞且釋放鎖線程

上圖中的等待隊列和互斥鎖是一一對應的,每一個互斥鎖都有本身的獨立的等待隊列(等待隊列是同一個)。(這句話還在暗示咱們後面喚醒線程時,是喚醒對應鎖上的線程。)

如何喚醒線程,notify()/notifyAll()

當條件知足時,咱們調用notify()或者notifyAll(),通知等待隊列(互斥鎖的等待隊列)中的線程,告訴它條件曾經知足過

咱們要在相應的鎖上使用wait() 、notify()和notifyAll()。
須要注意,這三個方法能夠被調用的前提是咱們已經獲取到了相應的互斥鎖。因此,咱們會發現wait() 、notify() notifyAll()都是在synchronized{...}內部中被調用的。若是在synchronized外部調用,JVM會拋出異常:java.lang.IllegalMonitorStateException。

使用「等待-通知」機制重寫轉帳

咱們如今使用「等待—通知」機制來優化上篇的一直循環獲取鎖的方案。首先咱們要清楚以下以下四點:

  1. 互斥鎖:帳本管理員Allocator是單例,因此咱們可使用this做爲互斥鎖;
  2. 線程要求的條件:轉出帳戶和轉入帳戶都存在,沒有被分配出去;
  3. 什麼時候等待:線程要求的條件不知足則等待;
  4. 什麼時候通知:當有線程歸還帳戶時就通知;

使用「等待—通知」機制時,咱們通常會套用一個「範式」,能夠看做是前人的經驗總結用法。

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()的區別

sleep()wait()均可以使線程阻塞,可是它們仍是有很大的區別:

  1. wait()方法會使當前線程釋放鎖,而sleep()方法則不會。
    當調用wait()方法後,當前線程會暫停執行,並進入互斥鎖的等待隊列中,直到有線程調用了notify()或者notifyAll(),等待隊列中的線程纔會被喚醒,從新競爭鎖。
    sleep()方法的調用須要指定等待的時間,它讓當前正在執行的線程在指定的時間內暫停執行,進入阻塞狀態,可是它不會使線程釋放鎖,這意味其餘線程在當前線程阻塞的時候,是不能進入獲取鎖,執行同步代碼的。
  2. wait()只能在同步方法或者同步代碼塊中執行,而sleep()能夠在任何地方執行。
  3. 使用wait()無需捕獲異常,而使用sleep()則必須捕獲。
  4. wait()是Object類的方法,而sleep是Thread的方法。

爲何wait()、notify()、notifyAll()是定義在Object中,而不是Thread中?

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

相關文章
相關標籤/搜索