前言
熟悉 Java 併發包的人一定對 LockSupport 的 park/unpark
方法不會感到陌生,它是 Lock(AQS)的基石,給 Lock(AQS)提供了掛起/恢復當前執行緒的能力。
LockSupport 的 park/unpark
方法本質上是對 Unsafe 的 park/unpark
方法的簡單封裝,而後者是 native 方法,對 Java 程式來說是一個黑箱操作,那麼要想了解它的底層實現,就必須深入 Java 虛擬機器的原始碼。
本篇將介紹 park/unpark
方法在 Hotsport 虛擬機器中的具體實現。
Parker 原始碼除錯與分析
在 Hotspot 原始碼中,unsafe.cpp 檔案專門用於為 Java Unsafe 類中的各種 native 方法提供具體實現。
其中 park
方法的實現程式碼如下:
unpark
方法的實現程式碼如下:
兩者的核心操作都是通過委託當前執行緒所關聯的 Parker 物件來完成的(每個執行緒都會關聯一個自己的 Parker 物件),於是,Parker 物件的 park/unpark
方法就成為了我們的焦點。
下面我將聯合 Java 程式與 Hotspot 原始碼一起除錯,觀察 Parker 物件的 park/unpark
方法的內部操作。
其中 Java 程式的程式碼如下:
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
System.out.println("park開始");
LockSupport.park();
System.out.println("park結束");
}, "t1");
Thread t2 = new Thread(() -> {
System.out.println("unpark開始");
LockSupport.unpark(t1);
System.out.println("unpark結束");
}, "t2");
Scanner scanner = new Scanner(System.in);
String input;
System.out.println("輸入“1”啟動t1執行緒,輸入“2”啟動t2執行緒,輸入“quit”退出");
while (!(input = scanner.nextLine()).equals("quit")) {
if (input.equals("1")) {
if (t1.getState().equals(Thread.State.NEW)) {
t1.start();
}
} else if (input.equals("2")) {
if (t2.getState().equals(Thread.State.NEW)) {
t2.start();
}
}
}
}
我們採用遠端除錯的方式執行上面的 Java 程式,然後通過在控制檯輸入“1” 來啟動 t1 執行緒。當 t1 執行緒啟動後,LockSupport.park
方法就會得以執行。
如圖所示,當前 t1 執行緒停在了斷點處,即停在了 Parker::park
方法的第一條語句上。
我們來分析一下該方法主要做的事情。
它首先利用一個原子交換操作將計數器的值改為 0,同時檢查計數器的原值是否大於 0,如果大於 0,表示當前 Parker 物件的 unpark 方法先於 park 方法執行了(因為 unpark 方法會把計數器的值改為 1),那麼本次 park 方法將直接返回,表示取消本次操作。如果計數器的原值不大於 0,則繼續往下執行。
接著判斷當前執行緒是否被標記了中斷,如果是的話就直接返回,否則就通過 pthread_mutex_trylock
函式嘗試加 mutex 鎖,如果加鎖失敗也直接返回。(pthread_mutex_trylock
函式是一個系統呼叫,它會針對作業系統的一個互斥量進行加鎖,加鎖成功將返回 0)。
在我們的除錯中,以上所有條件判斷都不命中,於是執行緒順利地執行到了下圖所示的位置。
圖中斷點處的程式碼相當關鍵,它完成了對 pthread_cond_wait
函式的呼叫,該函式是 Linux 標準執行緒庫(libpthread.so)中的一個系統呼叫,它會使當前執行緒加入作業系統的條件等待佇列,同時釋放 mutex 鎖並使當前執行緒掛起。
Java 中的
wait
和await
方法提供了和pthread_cond_wait
函式同樣的功能,前者本質上是對後者的封裝。如果對pthread_cond_wait
函式的具體實現感興趣,可以參考: https://code.woboq.org/userspace/glibc/nptl/pthread_cond_wait.c.html
由於 pthread_cond_wait
函式會使當前執行緒掛起,所以在我點選 "Step Over" 之後,執行緒阻塞在了 pthread_cond_wait
函式上,並等待被喚醒。
下圖顯示了通過 jstack 命令列印的執行緒堆疊資訊,可以看到 t1 執行緒已經處於 waiting (parking) 狀態。
至此,park 操作暫時告一段落。
接下來,我們通過在控制檯輸入“2” 來啟動 t2 執行緒。當 t2 執行緒啟動後,LockSupport.unpark(t1)
就會得以執行。
如圖所示,當前 t2 執行緒停在了斷點處,即停在了 Parker::unpark
方法的第二行程式碼上。
該方法做的事情相對簡單,它先是給當前執行緒加鎖,然後將計數器的值改為 1,接著判斷 Parker 物件所關聯的執行緒是否被 park,如果是,則通過 pthread_mutex_signal
函式喚醒該執行緒,最後釋放鎖。
pthread_mutex_signal
函式通常與 pthread_cond_wait
函式配套使用,其作用是喚醒作業系統中在某個條件變數上等待著的執行緒。
當 unpark 操作完成後,之前被 park 的執行緒將恢復至執行狀態(需要先拿到 mutex 鎖),然後從 pthread_cond_wait
方法中返回,接著執行剩餘程式碼。下圖顯示了Parker::park
方法的剩餘程式碼。
可以看到,當執行緒恢復執行後,計數器的值會再次被置為 0,然後執行緒會釋放鎖,並結束整個 park 操作。
park/unpark 原理總結
每個執行緒都會關聯一個 Parker 物件,每個 Parker 物件都各自維護了三個角色:計數器、互斥量、條件變數。
park 操作:
獲取當前執行緒關聯的 Parker 物件。 將計數器置為 0,同時檢查計數器的原值是否為 1,如果是則放棄後續操作。 在互斥量上加鎖。 在條件變數上阻塞,同時釋放鎖並等待被其他執行緒喚醒,當被喚醒後,將重新獲取鎖。 當執行緒恢復至執行狀態後,將計數器的值再次置為 0。 釋放鎖。
unpark 操作:
獲取目標執行緒關聯的 Parker 物件(注意目標執行緒不是當前執行緒)。 在互斥量上加鎖。 將計數器置為 1。 喚醒在條件變數上等待著的執行緒。 釋放鎖。
補充:jstack 命令和 kill 命令
jstack 命令會給 Java 虛擬機器程式傳送一個 SIGQUIT 訊號,當 Java 虛擬機器收到訊號後,會另起一個執行緒專門執行列印執行緒堆疊的任務。如圖,從 GDB
標籤頁中可以觀察到 SIGQUIT 訊號。
在 Linux 中使用 kill -3 命令也可以實現和 jstack 命令幾乎一樣的效果,這是因為 kill 命令本身就是一個用於給程式傳送訊號的工具,只不過預設傳送的是 SIGTERM 訊號(終止訊號),該訊號用於終止一個程式。可以通過 kill -l 命令檢視所有可用訊號,kill -3 表示傳送 SIGQUIT 訊號。