前言
在Java 1.5之前,Java語言提供的唯一並發語言就是管程,Java 1.5之后提供的SDK並發包也是以管程為基礎的。除了Java之外,C/C++、C#等高級語言也都是支持管程的。
那么什么是管程呢?
見名知意,是指管理共享變量以及對共享變量操作的過程,讓它們支持並發。翻譯成Java領域的語言,就是管理類的狀態變量,讓這個類是線程安全的。
synchronized關鍵字和wait()、notify()、notifyAll()這三個方法是Java中實現管程技術的組成部分。記得學習操作系統時,在線程一塊還有信號量機制,管程在功能上和信號量及PV操作類似,屬於一種進程同步互斥工具。Java選擇管程來實現並發主要還是因為實現管程比較容易。
管程對應的英文是Monitor,直譯為“監視器”,而操作系統領域一般翻譯為“管程”。
在管程的發展史上,先后出現過三種不同的管程模型,分別是Hasen模型、Hoare模型和MESA模型。現在正在廣泛使用的是MESA模型。下面我們便介紹MESA模型。
MESA模型
管程中引入了條件變量的概念,而且每個條件變量都對應有一個等待隊列。條件變量和等待隊列的作用是解決線程之間的同步問題。
我們來看一個例子來理解這個模型。多個線程對一個共享隊列進行操作。
假設線程T1要執行出隊操作,但是這個操作要執行成功的前提是隊列不能為空。這個隊列不能為空就是管程里的條件變量。若是線程T1進入管程后發現隊列是空的,那它就需要在“隊列不空”這個條件變量的等待隊列中等待。
通過調用wait()
實現。若是用對象A代表“隊列不空”這個條件,那么線程T1需要調用A.wait()
,來將自己阻塞。
在線程T1進入條件變量的等待隊列后,是允許其他線程進入管程的。
再假設之后另外一個線程T2執行了入隊操作,入隊操作成功之后,“隊列不空”這個條件對於線程T1來說已經滿足了,此時線程T2要通知線程T1,告訴它調用需要的條件已經滿足了。
那么線程T2怎么通知線程T1?線程T2調用A.notify()
來通知A等待隊列中的一個線程,此時這個線程里面只有T1,所以notify喚醒的就是線程T1,如果當這個條件變量的等待隊列不止T1一個線程,我們就需要使用notifyAll()。
當線程T1得到通知后,會從等待隊列中出來,重新進入到入口等待隊列中。
使用代碼說明就如下:(代碼來自參考[1])
注意,await()
和前面的wait()
的語義是一樣的;signal()
和前面的notify()
語義是一樣的(沒有提到的signalAll()
和notifyAll()
語義也是一樣的)。
public class BlockedQueue<T>{
final Lock lock = new ReentrantLock();
// 條件變量:隊列不滿
final Condition notFull = lock.newCondition();
// 條件變量:隊列不空
final Condition notEmpty = lock.newCondition();
// 入隊
void enq(T x) {
lock.lock();
try {
while (隊列已滿){
// 等待隊列不滿
notFull.await();
}
// 省略入隊操作...
// 入隊后, 通知可出隊
notEmpty.signal();
}finally {
lock.unlock();
}
}
// 出隊
void deq(){
lock.lock();
try {
while (隊列已空){
// 等待隊列不空
notEmpty.await();
}
// 省略出隊操作...
// 出隊后,通知可入隊
notFull.signal();
}finally {
lock.unlock();
}
}
}
wait()的正確使用姿勢
對於MESA管程來說,有一個編程范式:
while(條件不滿足) {
wait();
}
我們在前面介紹等待-通知機制時就提到過這種范式。這個范式可以解決“條件曾將滿足過”這個問題。喚醒的時間和獲取到鎖繼續執行的時間是不一致的,被喚醒的線程再次執行時可能條件又不滿足了,所以循環檢驗條件。
MESA模型的wait()方法還有一個超時參數,為了避免線程進入等待隊列永久阻塞。
notify()和notifyAll()分別何時使用
滿足以下三個條件時,可以使用notify(),其余情況盡量使用notifyAll():
- 所有等待線程擁有相同的等待條件;
- 所有等待線程被喚醒后,執行相同的操作;
- 只需要喚醒一個線程。
三種管程模型在通知線程上的區別
Hasen模型、Hoare模型和MESA模型的一個核心區別是當條件滿足后,如何通知相關線程。
管程要求同一時刻只允許一個線程執行,那當線程T2的操作使得線程T1等待的條件滿足時,T1和T2究竟誰可以執行呢?
- 在Hasen模型里,要求notify()放在代碼的最后,這樣T2通知完T1后,T2就結束了,然后T1再執行這樣就可以保證同一時刻只有一個線程執行。
- 在Hoare模型里面,T2通知完T1后,T2阻塞,T1馬上執行;等T1執行完,再喚醒t2。比起Hasen模型,T2多了一次阻塞喚醒操作。
- 在MESA管程里,T2通知完T1后,T2還是會接着執行,T1並不立即執行,僅僅是從條件變量的等待隊列進入到入口等待隊列中(但是T1再次執行時,可能條件又不滿足了,所以需要循環防方式檢驗條件變量)。這樣的好處是:notify()代碼不用放到代碼的最后,T2也沒有多余的阻塞喚醒操作。
Java語言的內置管程synchronized
Java 參考了 MESA 模型,語言內置的管程(synchronized)對 MESA 模型進行了精簡。MESA 模型中,條件變量可以有多個,Java 語言內置的管程里只有一個條件變量。模型如下圖所示。(圖來自參考[1])
Java 內置的管程方案(synchronized)使用簡單,synchronized 關鍵字修飾的代碼塊,在編譯期會自動生成相關加鎖和解鎖的代碼,但是僅支持一個條件變量;而 Java SDK 並發包實現的管程支持多個條件變量,不過並發包里的鎖,需要我們自己進行加鎖和解鎖操作。
小結
開始本來打算不寫這篇學習筆記的,但是思考了一下,Java並發實現本就是源於操作系統中的管程,既然要好好介紹Java並發那么它的來源也應該要好好介紹一下。在學習一個知識的時候,其背后的理論也要好好掌握。
參考:
[1]極客時間專欄王寶令《Java並發編程實戰》