JVM 源碼分析(四):深入理解 park / unpark


前言

熟悉 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 中的 waitawait 方法提供了和 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 操作:

  1. 獲取當前線程關聯的 Parker 對象。
  2. 將計數器置為 0,同時檢查計數器的原值是否為 1,如果是則放棄后續操作。
  3. 在互斥量上加鎖。
  4. 在條件變量上阻塞,同時釋放鎖並等待被其他線程喚醒,當被喚醒后,將重新獲取鎖。
  5. 當線程恢復至運行狀態后,將計數器的值再次置為 0。
  6. 釋放鎖。

unpark 操作:

  1. 獲取目標線程關聯的 Parker 對象(注意目標線程不是當前線程)。
  2. 在互斥量上加鎖。
  3. 將計數器置為 1。
  4. 喚醒在條件變量上等待着的線程。
  5. 釋放鎖。

補充:jstack 命令和 kill 命令

jstack 命令會給 Java 虛擬機進程發送一個 SIGQUIT 信號,當 Java 虛擬機收到信號后,會另起一個線程專門執行打印線程堆棧的任務。如圖,從 GDB 標簽頁中可以觀察到 SIGQUIT 信號。

在 Linux 中使用 kill -3 命令也可以實現和 jstack 命令幾乎一樣的效果,這是因為 kill 命令本身就是一個用於給進程發送信號的工具,只不過默認發送的是 SIGTERM 信號(終止信號),該信號用於終止一個進程。可以通過 kill -l 命令查看所有可用信號,kill -3 表示發送 SIGQUIT 信號。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM