一、概述
在前面兩篇文章當中,我們介紹了synchronized
的基本使用和原理,但是在使用synchronized
保證資料一致性的同時,我們希望能夠讓執行緒之間進行一些互動邏輯,也是我們今天要介紹的等待/通知模型,那麼就需要使用到wait/notify
。
二、等待/通知相關方法
2.1 方法說明
下面,我們先介紹等待/通知機制的相關方法,首先要說明兩點:
- 這些都是
Object
定義的方法 - 呼叫這些方法的前提條件是:該執行緒已經獲得了
Object
物件所關聯的鎖,也就是說它們需要位於synchronized
修飾的同步程式碼塊中。
**(a) wait() **
呼叫該方法的執行緒進入等待狀態,並釋放它所獲取的物件鎖,只有出現這兩種情況之一,它才會從wait
方法中返回,否則將會一直處於等待狀態:
- 其它執行緒通過
notify / notifyAll
方法通知該執行緒,並且該執行緒獲取到了物件鎖 - 執行緒被中斷
(b) wait(long) / wait(long, int)
和wait
方法相同,差別是增加一種從wait
方法返回的情況:等待的時間已經到了,並且獲取到了物件鎖。
(c) notify()
通知位於等待佇列中的第一個執行緒,使其從wait()
方法返回,而被通知的執行緒的繼續執行需要等到它獲得物件所為止。
需要注意,呼叫notify
方法後,並不會立刻釋放它所持有的物件鎖,這需要等到它執行完同步程式碼塊為止。
(d) notifyAll()
與notify()
類似,但是它是通知所有在物件上等待的執行緒。
2.2 實現原理
通過上面的介紹,我們可以看到,在整個等待/通知機制當中,執行緒被掛起時主要有以下三種狀態:等待狀態、超時等待狀態、阻塞狀態,這些狀態都是通過synchroized
所修飾的物件來實現的。
在前面我們介紹synchronized
原理的時候,曾經說過每個物件都會和一個Monitor
相關聯,其實每個Monitor
又包含有兩個佇列:等待佇列和同步佇列,其中等待佇列中存放是進入等待狀態的執行緒,而同步佇列中存放的是等待獲取鎖的執行緒。
下面,我們通過一段簡單的虛擬碼來立即兩個執行緒的狀態轉換過程:
synchronized public void waitThread() {
//執行a方法.
wait();
//執行b方法
}
synchronized public void notifyThread() {
//執行c方法
notify();
//執行d方法
}
複製程式碼
我們有AB
兩個執行緒,我們模擬以下的一系列行為:
(1) A 執行緒執行 waitThread 方法
此時由於物件鎖沒有被任何執行緒持有,因此,A
執行緒成為物件鎖的持有者:
B
執行緒執行notifyThread
方法時,由於此時物件鎖已經被A
執行緒持有,因此它被加入到同步佇列中:
(3) A 執行緒執行 a 方法
**(4) A 執行緒執行 wait 方法 **
當A
執行緒執行wait
方法後,它會釋放物件鎖,並加入到同步佇列當中,而B
執行緒則成為物件鎖新的持有者:
(5) B 執行緒執行 c 方法
(6) B 執行緒執行 notify 方法
此時會喚醒等待佇列中A
執行緒,但是此時B
執行緒仍然持有物件鎖,因此,A
執行緒只能被加入到同步佇列:
(7) B 執行緒執行 d 方法
(8) B 執行緒從 notifyThread 方法返回
此時A
執行緒重新獲取到物件鎖,因此它被從同步佇列中取出,繼續執行接下來的邏輯:
(9) A 執行緒執行 b 方法
(10) A 執行緒從 waitThread 方法中返回
當A
執行緒從同步方法返回之後,那麼會釋放它所持有的鎖
三、等待/通知的經典正規化
對於等待/通知模型,我們可以總結出它的經典正規化,分別針對等待方和通知方。
3.1 等待方
等待方遵循如下的原則:
- 獲取物件的鎖
- 如果條件不滿足,那麼呼叫物件的
wait
方法,被通知後仍然需要檢查條件 - 條件滿足則繼續執行對應的邏輯
對應的虛擬碼為:
synchronized( 物件 ) {
while( 條件不滿足 ) {
物件.wait();
}
對應的處理邏輯
}
複製程式碼
3.2 通知方
通知方遵循如下的原則:
- 獲得物件的鎖
- 改變條件
- 通知所有等待在物件上的執行緒
synchronized( 物件 ) {
改變條件;
物件.notifyAll();
}
複製程式碼