操作系統使用信號量解決併發問題,Java選擇使用管程(Monitor)解決併發問題。信號量和管程是等價的,可以使用信號量實現管程,也可以使用管程實現信號量。
管程就是指管理共享變量,以及對共享變量的相關操作。具體到 Java 語言中,管程就是管理類的成員變量和方法,讓這個類是線程安全的。管程的發展史中,先後出現過三種管程模型,Hasen 模型、Hoare 模型和 MESA 模型,Java 使用的是 MESA 模型。
我們用管程模型主要是解決併發編程中的兩個核心問題,互斥和同步。互斥是指同一時刻只允許一個線程訪問共享資源,同步則是指線程之間如何通信、寫作。
那麼,Java 所採用的 MESA 模型是如何解決互斥和同步問題的呢?
MESA 解決互斥問題
管程模型解決互斥問題的方法是:將共享變量及對共享變量的操作統一封裝起來。
如下圖所示,管程 X 將共享變量 queue,及其入隊出隊操作 enq() 和 dep() 封裝起來。線程 A 和線程 B 想要訪問共享變量 queue,就需要通過 enq() 和 deq() 來實現,而 enq() 和 deq() 保證互斥,只允許一個線程進入管程。
MESA 解決同步問題
MESA 模型解決同步問題可以類比去醫院就醫。患者首先需要排隊等待醫生叫好,醫生診斷被叫到號的患者。期間,患者如果需要進行其他輔助的檢查,比如說排個 X 光,就需要去等待拍 X 光的醫生叫好。患者拍完 X 光之後,再次回到上一個醫生那裏,等待醫生再次診斷。
管程模型與看醫生的流程類似,管程入口處有一個等待隊列。當多個線程試圖進入管程內部的時候,只允許一個線程進入,其他線程在等待隊列中等待。就和看醫生的時候排隊一樣。
管程中還有一個條件變量的概念,每個條件變量對應一個條件變量等待隊列。比如說有一個條件變量 A,當執行線程 T1 時發現不滿足條件變量 A,T1 就會進入條件變量 A 的等待隊列中。就像去看醫生,醫生讓你先去排個 X 光,就要去拍 X 光的地方排隊。
當執行線程 T2 時發現滿足條件變量 A,就會喚醒條件變量 A 等待中的線程 T1,線程 T1 就會再次進入到入口等待隊列。就像拍完 X 光的人,再去看醫生。
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();
}
}
}
- 對於入隊操作,如果隊列已滿,就需要等待直到隊列不滿,所以這裏用了notFull.await();。
- 對於出隊操作,如果隊列爲空,就需要等待直到隊列不空,所以就用了notEmpty.await();。
- 如果入隊成功,那麼隊列就不空了,就需要通知條件變量:隊列不空notEmpty對應的等待隊列。
- 如果出隊成功,那就隊列就不滿了,就需要通知條件變量:隊列不滿notFull對應的等待隊列。
synchronized 單條件變量的管程模型
Java 參考了 MESA 模型,語言內置的管程(synchronized)對 MESA 模型進行了精簡。MESA 模型中,條件變量可以有多個,Java 語言內置的管程裏只有一個條件變量。
Java SDK 併發包實現的管程支持多個條件變量,不過併發包裏的鎖,需要開發人員自己進行加鎖和解鎖操作。