前言
熟悉 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 信號。