wait()、notify()方法原理,以及使用注意事項--丟失喚醒、虛假喚醒


wait、notify原理

在前面以經說到對象鎖的本質,重量級鎖模式時對象頭是一個指向互斥量的指針,實際上互斥量就是一個監視器鎖(ObjectMonitor)的數據結構,此時對象的hashCode、分代年齡等信息都會保存到對應的ObjectMonitor中,ObjectMonitor還有一些屬性如recursion記錄本鎖被重入的次數,EntrySet記錄想獲取本鎖的線程集合,WaitSet記錄等待本鎖的線程,TheOwner記錄擁有本鎖的線程對象。如下:

(圖片來源於網絡)

幾個線程一起競爭對象的鎖(EntrySet),只有一個能成功(acquire),成功的線程記錄在The Owner中。調用wait、notify運行流程如下:

       (1) 現有一個對象o,鎖正在被線程 t1 持有,調用wait()方法后,線程 t1 將會被"晾到" (實際上僅僅是記錄到) Wait Set 結構中。

       (2)然后將會有另一個線程 t2 獲取到鎖,The Owner記錄的變成了 t2 線程。

       (3)t2 線程不需要 o的時,調用o.notify()/o.notifyAll()方法,對象o就會告訴 Wait Set結構中記錄的線程們:你們又可以來競爭我啦,我的鎖現在沒被人持有。

簡單的說就是:wait是對象通知持有自己鎖的線程釋放我的鎖,notify()/notifyAll()就是對象通知剛剛被自己晾在一邊的線程又可以來競爭我的鎖了。我想到了一個比較貼切的比喻:

        客人(線程)來拜訪主人(對象),必須獲得主人的時間權(鎖),且主人同時只能接待一人(互斥)。

        正在客廳接待一名客人時,因為一些原因主人必須先接待另一位客人,這時主人請當前客人去另一間房里等待,讓出自己的時間權(wait方法)

        主人在客廳接待另一位客人,接待完畢后,讓前一位(也可能有幾位)在另一間房等待的客人再來到客廳,繼續接待(notify/notifyAll方法)

 

wait、notify要放在同步塊中

其實很簡單,如果不在同步塊中,調用this.wait()時當前線程都沒有取得對象的鎖,又談何讓對象通知線程釋放鎖、或者來競爭鎖呢?如果確實不放到同步塊中,則會產生 Lost-wake的問題,即丟失喚醒,以上一篇中生產者消費者例子來說:

       1 箱子發現自己滿了調用box.wait()通知生產者等待,但是由於wait沒在同步塊中,還沒等生產者接到wait信號進入等待,消費者線程就插隊執行消費箱子蘋果的方法了(因為wait不在同步塊中,也就是調用時箱子的鎖沒被占有,所以箱子的消費方法是可以被消費者插隊調用的)。

        2 這時消費者線程從緩沖區消費一個產品后箱子調用box.notify()方法,但生產者此時還沒進入等待,因此notify消息將被生產者忽略。

      3  生產者線程恢復執行接收到遲來的wait()信號后進入等待狀態,但是得不到notify通知了,一直等待下去。

總結就是,由於wait不在同步塊中,所以對象執行wait()到線程接到通知進入等待這段時間是可以被其他線程插隊,如果這時插隊的線程把notify信號發出則會被忽略,因為本來要被wait的線程還在卡着呢。總之,這里的競爭條件,我們可能在丟失一個通知,如果我們使用緩沖區或 者只有一個產品,生產者線程將永遠等待,你的程序也就掛起了。
 
虛假喚醒
notify/notifyAll時喚醒的線程並不一定是滿足真正可以執行的條件了。比如對象o,某線程不滿足A條件時發出o.wait(),然后另一線程不滿足條件B時也發出o.wait;后來某時間條件B滿足了,發出o.notify(),喚醒對象o的等待池里的對象,但是喚醒的線程有可能是因為條件A進入等待的線程,這時把他喚醒條件但是A還是不滿足。這是底層系統決定的一個小遺憾。為了避免這種情況,判斷調用o.wait()的條件時必須使用while,而不是if,這樣在虛假喚醒后會繼續判斷是否滿足A條件,不滿足說明是虛假喚醒又會調用o.wait()。
//生產蘋果的方法的代碼:
while(箱子滿了){   //使用while而不是if
    box.wait();      
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM