Java 中的 Monitor 機制
monitor的概念
管程,英文是 Monitor,也常被翻譯為“監視器”,monitor 不管是翻譯為“管程”還是“監視器”,都是比較晦澀的,通過翻譯後的中文,並無法對 monitor 達到一個直觀的描述。在《作業系統同步原語》 這篇文章中,介紹了作業系統在面對 程序/執行緒 間同步的時候,所支援的一些同步原語,其中 semaphore 訊號量 和 mutex 互斥量是最重要的同步原語。在使用基本的 mutex 進行併發控制時,需要程式設計師非常小心地控制 mutex 的 down 和 up 操作,否則很容易引起死鎖等問題。為了更容易地編寫出正確的併發程式,所以在 mutex 和 semaphore 的基礎上,提出了更高層次的同步原語 monitor,不過需要注意的是,作業系統本身並不支援 monitor 機制,實際上,monitor 是屬於程式語言的範疇,當你想要使用 monitor 時,先了解一下語言本身是否支援 monitor 原語,例如 C 語言它就不支援 monitor,Java 語言支援 monitor。一般的 monitor 實現模式是程式語言在語法上提供語法糖,而如何實現 monitor 機制,則屬於編譯器的工作,Java 就是這麼幹的。
monitor 的重要特點是,同一個時刻,只有一個 程序/執行緒 能進入 monitor 中定義的臨界區,這使得 monitor 能夠達到互斥的效果。但僅僅有互斥的作用是不夠的,無法進入 monitor 臨界區的 程序/執行緒,它們應該被阻塞,並且在必要的時候會被喚醒。顯然,monitor 作為一個同步工具,也應該提供這樣的管理 程序/執行緒 狀態的機制。想想我們為什麼覺得 semaphore 和 mutex 在程式設計上容易出錯,因為我們需要去親自操作變數以及對 程序/執行緒 進行阻塞和喚醒。monitor 這個機制之所以被稱為“更高階的原語”,那麼它就不可避免地需要對外遮蔽掉這些機制,並且在內部實現這些機制,使得使用 monitor 的人看到的是一個簡潔易用的介面。
monitor 基本元素
monitor 機制需要幾個元素來配合,分別是:
- 臨界區
- monitor 物件及鎖
- 條件變數以及定義在 monitor 物件上的 wait,signal 操作。
使用 monitor 機制的目的主要是為了互斥進入臨界區,為了做到能夠阻塞無法進入臨界區的 程序/執行緒,還需要一個 monitor object 來協助,這個 monitor object 內部會有相應的資料結構,例如列表,來儲存被阻塞的執行緒;同時由於 monitor 機制本質上是基於 mutex 這種基本原語的,所以 monitor object 還必須維護一個基於 mutex 的鎖。此外,為了在適當的時候能夠阻塞和喚醒 程序/執行緒,還需要引入一個條件變數,這個條件變數用來決定什麼時候是“適當的時候”,這個條件可以來自程式程式碼的邏輯,也可以是在 monitor object 的內部,總而言之,程式設計師對條件變數的定義有很大的自主性。不過,由於 monitor object 內部採用了資料結構來儲存被阻塞的佇列,因此它也必須對外提供兩個 API 來讓執行緒進入阻塞狀態以及之後被喚醒,分別是 wait 和 notify。
Java 語言對 monitor 的支援
monitor 是作業系統提出來的一種高階原語,但其具體的實現模式,不同的程式語言都有可能不一樣。以下以 Java 的 monitor 為例子,來講解 monitor 在 Java 中的實現方式。
臨界區的圈定
在 Java 中,可以採用 synchronized 關鍵字來修飾例項方法、類方法以及程式碼塊,如下所示:
/**
* @author beanlam
* @version 1.0
* @date 2018/9/12
*/
public class Monitor {
private Object ANOTHER_LOCK = new Object();
private synchronized void fun1() {
}
public static synchronized void fun2() {
}
public void fun3() {
synchronized (this) {
}
}
public void fun4() {
synchronized (ANOTHER_LOCK) {
}
}
}
被 synchronized 關鍵字修飾的方法、程式碼塊,就是 monitor 機制的臨界區。
monitor object
可以發現,上述的 synchronized 關鍵字在使用的時候,往往需要指定一個物件與之關聯,例如 synchronized(this),或者 synchronized(ANOTHER_LOCK),synchronized 如果修飾的是例項方法,那麼其關聯的物件實際上是 this,如果修飾的是類方法,那麼其關聯的物件是 this.class。總之,synchronzied 需要關聯一個物件,而這個物件就是 monitor object。monitor 的機制中,monitor object 充當著維護 mutex以及定義 wait/signal API 來管理執行緒的阻塞和喚醒的角色。Java 語言中的 java.lang.Object 類,便是滿足這個要求的物件,任何一個 Java 物件都可以作為 monitor 機制的 monitor object。Java 物件儲存在記憶體中,分別分為三個部分,即物件頭、例項資料和對齊填充,而在其物件頭中,儲存了鎖標識;同時,java.lang.Object 類定義了 wait(),notify(),notifyAll() 方法,這些方法的具體實現,依賴於一個叫 ObjectMonitor 模式的實現,這是 JVM 內部基於 C++ 實現的一套機制,基本原理如下所示:
當一個執行緒需要獲取 Object 的鎖時,會被放入 EntrySet 中進行等待,如果該執行緒獲取到了鎖,成為當前鎖的 owner。如果根據程式邏輯,一個已經獲得了鎖的執行緒缺少某些外部條件,而無法繼續進行下去(例如生產者發現佇列已滿或者消費者發現佇列為空),那麼該執行緒可以通過呼叫 wait 方法將鎖釋放,進入 wait set 中阻塞進行等待,其它執行緒在這個時候有機會獲得鎖,去幹其它的事情,從而使得之前不成立的外部條件成立,這樣先前被阻塞的執行緒就可以重新進入 EntrySet 去競爭鎖。這個外部條件在 monitor 機制中稱為條件變數。
synchronized 關鍵字
synchronized 關鍵字是 Java 在語法層面上,用來讓開發者方便地進行多執行緒同步的重要工具。要進入一個 synchronized 方法修飾的方法或者程式碼塊,會先獲取與 synchronized 關鍵字繫結在一起的 Object 的物件鎖,這個鎖也限定了其它執行緒無法進入與這個鎖相關的其它 synchronized 程式碼區域。
網上很多文章以及資料,在分析 synchronized 的原理時,基本上都會說 synchronized 是基於 monitor 機制實現的,但很少有文章說清楚,都是模糊帶過。參照前面提到的 Monitor 的幾個基本元素,如果 synchronized 是基於 monitor 機制實現的,那麼對應的元素分別是什麼?它必須要有臨界區,這裡的臨界區我們可以認為是對物件頭 mutex 的 P 或者 V 操作,這是個臨界區那 monitor object 對應哪個呢?mutex?總之無法找到真正的 monitor object。所以我認為“synchronized 是基於 monitor 機制實現的”這樣的說法是不正確的,是模稜兩可的。Java 提供的 monitor 機制,其實是 Object,synchronized 等元素合作形成的,甚至說外部的條件變數也是個組成部分。JVM 底層的 ObjectMonitor 只是用來輔助實現 monitor 機制的一種常用模式,但大多數文章把 ObjectMonitor 直接當成了 monitor 機制。我覺得應該這麼理解:Java 對 monitor 的支援,是以機制的粒度提供給開發者使用的,也就是說,開發者要結合使用 synchronized 關鍵字,以及 Object 的 wait / notify 等元素,才能說自己利用 monitor 的機制去解決了一個生產者消費者的問題。