Wait-Notify場景
典型的Wait-Notify場景一般與以下兩個內容相關:
1. 狀態變量(State Variable)
當線程需要wait的時候,總是因為一些條件得不到滿足導致的。例如往隊列裡填充數據,當隊列元素已經滿時,線程就需要wait停止運行。當隊列元素有空缺時,再繼續自己的執行。
2. 條件斷言(Condition Predicate)
當線程確定是否進入wait或者是從notify醒來的時候是否繼續往下執行,大部分都要測試狀態條件是否滿足。例如,往隊列裡添加元素,隊列已滿,於是阻塞當前線程,當有其他線程從隊列裡取走了元素,就通知在等待的線程“隊列有剩餘空間,可以往裡添加元素了”。這時,等待添加元素的進程就會被喚醒,然後判斷一下當前隊列是否真的有剩餘空間,如果真的有剩餘空間,就將元素添加進去,如果沒有,則繼續阻塞等待下次喚醒。
3. 條件隊列(Condition Queue)
每個對像都有一個內置的條件隊列,當一個線程在該對象鎖上調用wait函數的時候,就會將該線程加入到該對象的條件隊列中。
注意
wait與notify是Java同步機制中的重要組成部分。結合與synchronized關鍵字使用,可以建立很多優秀的同步模型,例如生產者-消費者模型。但是在使用wait()、notify()、notifyAll()函數的時候,需要特別注意以下幾點:
wait()、notify()、notifyAll()方法不屬於Thread類,而是屬於Object基礎類,也就是說每個對像都有wait()、notify()、notifyAll()的功能。因為每個對像都有鎖,鎖是每個對象的基礎,因此操作鎖的方法也是最基礎的。
調用obj的wait(), notify()方法前,必須獲得obj鎖,也就是必須寫在synchronized(obj){...} 代碼段內。
調用obj.wait()後,線程A就釋放了obj的鎖,否則線程B無法獲得obj鎖,也就無法在synchronized(obj){...} 代碼段內喚醒線程A。
當obj.wait()方法返回後,線程A需要再次獲得obj鎖,才能繼續執行。
如果線程A1,A2,A3都在obj.wait(),則線程B調用obj.notify()只能喚醒線程A1,A2,A3中的一個(具體哪一個由JVM決定)。
如果線程B調用obj.notifyAll()則能全部喚醒等待的線程A1,A2,A3,但是等待的線程要繼續執行obj.wait()的下一條語句,必須獲得obj鎖。因此,線程A1,A2,A3只有一個有機會獲得鎖繼續執行,例如A1,其餘的需要等待A1釋放obj鎖之後才能繼續執行。
當線程B調用obj.notify()或者obj.notifyAll()的時候,線程B正持有obj鎖,因此,線程A1,A2,A3雖被喚醒,但是仍無法獲得obj鎖。直到線程B退出synchronized代碼塊,釋放obj鎖後,線程A1,A2,A3中的一個才有機會獲得對象鎖並得以繼續執行。
示例代碼
線程的wait操作的典型代碼結構如下:
public void test() throws InterruptedException { synchronized(obj) { while (! contidition) { obj.wait(); } } }
為什麼obj.wait()操作必須位於循環中呢?有以下幾個主要原因:
1. 一個對象鎖可能用於保護多個狀態變量,當它們都需要wait-notify操作時,如果不將wait放到while循環中就會有問題。例如,某對象鎖obj保護兩種狀態變量a和b,當a的條件斷言不成立時發生了wait操作,當b的條件斷言不成立時也發生了wait操作,兩個線程被加入到obj對應的條件隊列中。現在若改變狀態變量a的某操作發生,在obj上調用了notifyAll操作,則obj對應的條件隊列裡的所有線程均被喚醒,之前等待a的一個或幾個線程去判斷a的條件斷言可能成立了,但是b對於的條件斷言肯定仍不成立,而此時等待b的線程也被喚醒了,所以需要循環判斷b的條件斷言是否滿足,如果不滿足,則繼續wait。
2. 多個線程wait的同一狀態的條件斷言。例如,向隊列添加元素的場景,當前隊列是滿的,多個線程想往裡面添加元素,於是都wait了。此時,另一個線程從隊列裡取出了一個元素,調用了notifyAll操作,喚醒了所有線程,但是只有一個線程能夠往隊列裡添加一個元素,其他的仍需要等待。
3. 虛假喚醒。在沒有被通知、中斷或超時的情況下,線程自動甦醒了。雖然這種情況在實踐中很少發生,但是通過循環等待可以杜絕這一情況的發生。