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的線程還在卡着呢。總之,這里的競爭條件,我們可能在丟失一個通知,如果我們使用緩沖區或 者只有一個產品,生產者線程將永遠等待,你的程序也就掛起了。//生產蘋果的方法的代碼: while(箱子滿了){ //使用while而不是if box.wait(); }