(2.1.27.7)Java併發程式設計:Object.wait/notify
Java Object物件中的wait,notify,notifyAll是定義在Object類的例項方法,用於控制執行緒狀態。
三個方法都必須在synchronized 同步關鍵字所限定的作用域中呼叫,否則會報錯java.lang.IllegalMonitorStateException ,意思是因為沒有同步,所以執行緒對物件鎖的狀態是不確定的,不能呼叫這些方法。
- wait
- 表示持有物件鎖的執行緒A準備釋放物件鎖許可權,釋放cpu資源並進入等待。
- notify
- 表示持有物件鎖的執行緒A準備釋放物件鎖許可權,通知jvm喚醒某個隨機競爭該物件鎖的執行緒X。
- 執行緒A synchronized 程式碼作用域結束後,執行緒X直接獲得物件鎖許可權,其他競爭執行緒繼續等待(即使執行緒X同步完畢,釋放物件鎖,其他競爭執行緒仍然等待,直至有新的notify ,notifyAll被呼叫)。
- notifyAll
- 表示持有物件鎖的執行緒A準備釋放物件鎖許可權,通知jvm喚醒所有競爭該物件鎖的執行緒
- 執行緒A synchronized 程式碼作用域結束後,jvm通過演算法將物件鎖許可權指派給某個執行緒X,所有被喚醒的執行緒不再等待。執行緒X synchronized 程式碼作用域結束後,之前所有被喚醒的執行緒都有可能獲得該物件鎖許可權,這個由JVM演算法決定
我們不禁產生這樣的疑問:
- 進入wait/notify方法之前,為什麼要獲取synchronized鎖?
- 執行緒A獲取了synchronized鎖,執行wait方法並掛起,執行緒B又如何再次獲取鎖?
一、示例
public class WaitNotifyCase { public static void main(String[] args) { final Object lock = new Object(); new Thread(new Runnable() { @Override public void run() { System.out.println("thread A is waiting to get lock"); synchronized (lock) { try { System.out.println("thread A get lock"); TimeUnit.SECONDS.sleep(1); System.out.println("thread A do wait method"); lock.wait(); System.out.println("wait end"); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println("thread B is waiting to get lock"); synchronized (lock) { System.out.println("thread B get lock"); try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } lock.notify(); System.out.println("thread B do notify method"); } } }).start(); } } 執行結果: thread A is waiting to get lock thread A get lock thread B is waiting to get lock thread A do wait method thread B get lock thread B do notify method wait end
由同一個lock物件呼叫wait、notify方法。 1、當執行緒A執行wait方法時,該執行緒會被掛起; 2、當執行緒B執行notify方法時,會喚醒一個被掛起的執行緒A;
lock物件、執行緒A和執行緒B三者是一種什麼關係?根據上面的結論,可以想象一個場景: 1、lock物件維護了一個等待佇列list; 2、執行緒A中執行lock的wait方法,把執行緒A儲存到list中; 3、執行緒B中執行lock的notify方法,從等待佇列中取出執行緒A繼續執行;
二、為什麼要使用synchronized?
我們知道wait/notify是為了執行緒間協作而設計的,當我們執行wait的時候讓執行緒掛起,當執行notify的時候喚醒其中一個掛起的執行緒,那需要有個地方來儲存物件和執行緒之間的對映關係(可以想象一個map,key是物件,value是一個執行緒列表)
當呼叫這個物件的wait方法時,將當前執行緒放到這個執行緒列表裡,當呼叫這個物件的notify方法時從這個執行緒列表裡取出一個來讓其繼續執行
這樣看來是可行的,也比較簡單,那現在的問題這種對映關係放到哪裡?
lock.wait()方法通過呼叫native方法wait(0)實現,其中介面註釋中有這麼一句:
The current thread must own this object’s monitor.
表示執行緒執行lock.wait()方法時,必須持有該lock物件的ObjectMonitor(前文已經提及,每個鎖物件(這裡指已經升級為重量級鎖的物件)都有一個ObjectMonitor(物件監視器)。也就是說每個執行緒獲取鎖物件都會通過ObjectMonitor)
如果wait方法在synchronized程式碼中執行,該執行緒很顯然已經持有了monitor。
從而包含了“ 進入wait/notify方法之前,為什麼要獲取synchronized鎖”,synchronized程式碼塊通過javap生成的位元組碼中包含 *monitorenter 和 monitorexit指令。其中執行monitorenter指令可以獲取物件的monitor
【為什麼要使用synchronized】
三、程式碼執行過程分析
- 在多核環境下,執行緒A和B有可能同時執行monitorenter指令,並獲取lock物件關聯的monitor,只有一個執行緒可以和monitor建立關聯,假設執行緒A執行加鎖成功;
- 執行緒B競爭加鎖失敗,進入等待佇列進行等待;
- 執行緒A繼續執行,當執行到wait方法
3.1 Object.wait方法實現
wait方法會將當前執行緒放入wait set,等待被喚醒,並放棄lock物件上的所有同步宣告,意味著"執行緒A釋放了鎖,執行緒B可以重新執行加鎖操作"
Object.wait()在使用時通常要判斷是否滿足某個條件,不滿足某個外部條件cond時呼叫wait(),來讓執行緒阻塞同時釋放被synchronized鎖定的mutex;從這個過程看來Object.wait()實際上是起到條件變數的作用
- wait()內部實際上先將synchronized鎖定的鎖釋放
- 之後將當前執行緒阻塞在某個內建的條件condition上(注意:此condition為內建的,與外部判斷的條件cond並非同一個,外部的cond需要程式設計師根據程式邏輯來判斷改變,而這個condition只能被Object.notify()/notifyAll()改變),直到內建條件condition被Object.notify()/notifyAll()修改時才會重新鎖定該mutex,繼續執行wait()後的程式碼。
wait() {
unlock(mutex);//解鎖mutex
wait_condition(condition);//等待內建條件變數condition
lock(mutex);//競爭鎖
}
lock.wait()方法最終通過ObjectMonitor的void wait(jlong millis, bool interruptable, TRAPS);實現:
1、將當前執行緒封裝成ObjectWaiter物件node; 【wait方法實現1】
2、通過ObjectMonitor::AddWaiter方法將node新增到_WaitSet列表中;
【wait方法實現2】
3、通過ObjectMonitor::exit方法釋放當前的ObjectMonitor物件,這樣其它競爭執行緒就可以獲取該ObjectMonitor物件。
【wait方法實現3】
4、最終底層的park方法會掛起執行緒; 5、ObjectMonitor中的其他競爭執行緒被CPU自動喚醒和選擇某個啟動執行
3.2 Object.notify()/notifyAll方法實現
Object.notify()/notifyAll()實際上只起到一個sinal內建條件變數的作用,呼叫Object.notify()/notifyAll()之後,這個時候其他處於wait()中的執行緒所等待的內建條件變數已經滿足,但是由於wait()中仍然需要lock mutex
而在Object.notify()/notifyAll()中沒有把mutex釋放掉,故阻塞在wait()處的執行緒繼續等待,但等待的條件不再是內建條件變數而是鎖mutex;直到synchronized程式碼塊結束時,由於會自動釋放被synchronized鎖定的mutex,故此時所有在wait()中等待mutex的執行緒開始競爭mutex,得到該mutex的會繼續執行,否則繼續等待mutex
obj.notify()/notifyAll(){
condition=true;//只起到把內建條件變數置為true的作用
}
-
lock.notify()方法最終通過ObjectMonitor的void notify(TRAPS)實現:
- 如果當前_WaitSet為空,即沒有正在等待的執行緒,則直接返回;
- 通過ObjectMonitor::DequeueWaiter方法,獲取_WaitSet列表中的第一個ObjectWaiter節點,實現也很簡單。這裡需要注意的是,在jdk的notify方法註釋是隨機喚醒一個執行緒,其實是第一個ObjectWaiter節點
- 根據不同的策略,將取出來的ObjectWaiter節點,加入到_EntryList或則通過Atomic::cmpxchg_ptr指令進行自旋操作cxq,具體程式碼實現有點長,這裡就不貼了,有興趣的同學可以看objectMonitor::notify方法;
【Object.notify()notifyAll方法實現】
-
lock.notifyAll()方法最終通過ObjectMonitor的void notifyAll(TRAPS)實現:
- 通過for迴圈取出_WaitSet的ObjectWaiter節點,並根據不同策略,加入到_EntryList或則進行自旋操作。
從JVM的方法實現中,可以發現:notify和notifyAll並不會釋放所佔有的ObjectMonitor物件. 其實真正釋放ObjectMonitor物件的時間點是在執行monitorexit指令,一旦釋放ObjectMonitor物件了,entry set中ObjectWaiter節點所儲存的執行緒就可以開始競爭ObjectMonitor物件進行加鎖操作了。
- 被notify(All)的執行緒有規律嗎
- 如果是通過notify來喚起的執行緒,那先進入wait的執行緒會先被喚起來
- 如果是通過nootifyAll喚起的執行緒,預設情況是最後進入的會先被喚起來,即LIFO的策略