博客 / 詳情

返回

LockSupport深度解析:線程阻塞與喚醒的底層實現原理

LockSupport簡介

LockSupprot 用來阻塞和喚醒線程,底層實現依賴於 Unsafe 類。

LockSupport用來創建鎖和其他同步類的基本線程阻塞原語。簡而言之,當調用LockSupport.park時,表示當前線程將會等待,直至獲得許可,當調用LockSupport.unpark時,必須把等待獲得許可的線程作為參數進行傳遞,好讓此線程繼續運行。在AQS中大量使用,AQS最終都是使用LockSupport來阻塞線程的。

該類包含一組用於阻塞和喚醒線程的靜態方法,這些方法主要是圍繞 park 和 unpark 展開,話不多説,直接來看一個簡單的例子吧。

public class LockSupportDemo1 {
    public static void main(String[] args) {
        Thread mainThread = Thread.currentThread();

        // 創建一個線程從1數到1000
        Thread counterThread = new Thread(() -> {
            for (int i = 1; i <= 1000; i++) {
                System.out.println(i);
                if (i == 500) {
                    // 當數到500時,喚醒主線程
                    LockSupport.unpark(mainThread);
                }
            }
        });

        counterThread.start();

        // 主線程調用park
        LockSupport.park();
        System.out.println("Main thread was unparked.");
    }
}

上面的代碼中,當 counterThread 數到 500 時,它會喚醒 mainThread。而 mainThread 在調用 park 方法時會被阻塞,直到被 unpark。

LockSupport 中的方法不多,這裏將這些方法做一個總結:

阻塞線程

  1. void park():阻塞當前線程,如果調用 unpark 方法或線程被中斷,則該線程將變得可運行。請注意,park 不會拋出 InterruptedException,因此線程必須單獨檢查其中斷狀態。
  2. void park(Object blocker):功能同方法 1,入參增加一個 Object 對象,用來記錄導致線程阻塞的對象,方便問題排查。
  3. void parkNanos(long nanos):阻塞當前線程一定的納秒時間,或直到被 unpark 調用,或線程被中斷。
  4. void parkNanos(Object blocker, long nanos):功能同方法 3,入參增加一個 Object 對象,用來記錄導致線程阻塞的對象,方便問題排查。
  5. void parkUntil(long deadline):阻塞當前線程直到某個指定的截止時間(以毫秒為單位),或直到被 unpark 調用,或線程被中斷。
  6. void parkUntil(Object blocker, long deadline):功能同方法 5,入參增加一個 Object 對象,用來記錄導致線程阻塞的對象,方便問題排查。

喚醒線程

void unpark(Thread thread):喚醒一個由 park 方法阻塞的線程。如果該線程未被阻塞,那麼下一次調用 park 時將立即返回。這允許“先發制人”式的喚醒機制。

實際上,LockSupport 阻塞和喚醒線程的功能依賴於 sun.misc.Unsafe,這是一個很底層的類,比如 LockSupport 的 park 方法是通過 unsafe.park() 方法實現的。

LockSupport源碼分析

類的屬性

public class LockSupport {
    // Hotspot implementation via intrinsics API
    private static final sun.misc.Unsafe UNSAFE;
    // 表示內存偏移地址
    private static final long parkBlockerOffset;
    // 表示內存偏移地址
    private static final long SEED;
    // 表示內存偏移地址
    private static final long PROBE;
    // 表示內存偏移地址
    private static final long SECONDARY;
    
    static {
        try {
            // 獲取Unsafe實例
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            // 線程類類型
            Class<?> tk = Thread.class;
            // 獲取Thread的parkBlocker字段的內存偏移地址
            parkBlockerOffset = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("parkBlocker"));
            // 獲取Thread的threadLocalRandomSeed字段的內存偏移地址
            SEED = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomSeed"));
            // 獲取Thread的threadLocalRandomProbe字段的內存偏移地址
            PROBE = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomProbe"));
            // 獲取Thread的threadLocalRandomSecondarySeed字段的內存偏移地址
            SECONDARY = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomSecondarySeed"));
        } catch (Exception ex) { throw new Error(ex); }
    }
}

説明: UNSAFE字段表示sun.misc.Unsafe類,一般程序中不允許直接調用,而long型的表示實例對象相應字段在內存中的偏移地址,可以通過該偏移地址獲取或者設置該字段的值。

類的構造函數

// 私有構造函數,無法被實例化
private LockSupport() {}

説明: LockSupport只有一個私有構造函數,無法被實例化。

核心函數分析

在分析LockSupport函數之前,先引入sun.misc.Unsafe類中的park和unpark函數,因為LockSupport的核心函數都是基於Unsafe類中定義的park和unpark函數,下面給出兩個函數的定義:

public native void park(boolean isAbsolute, long time);
public native void unpark(Thread thread);

説明: 對兩個函數的説明如下:

  • park函數,阻塞線程,並且該線程在下列情況發生之前都會被阻塞:

    • 調用unpark函數,釋放該線程的許可。

    • 該線程被中斷。

    • 設置的時間到了。並且,當time為絕對時間時,isAbsolute為true,否則,isAbsolute為false。當time為0時,表示無限等待,直到unpark發生。

  • unpark函數,釋放線程的許可,即激活調用park後阻塞的線程。這個函數不是安全的,調用這個函數時要確保線程依舊存活。

park函數

park函數有兩個重載版本,方法摘要如下

public static void park();
public static void park(Object blocker);

説明: 兩個函數的區別在於park()函數沒有沒有blocker,即沒有設置線程的parkBlocker字段。park(Object)型函數如下。

public static void park(Object blocker) {
    // 獲取當前線程
    Thread t = Thread.currentThread();
    // 設置Blocker
    setBlocker(t, blocker);
    // 獲取許可
    UNSAFE.park(false, 0L);
    // 重新可運行後再此設置Blocker
    setBlocker(t, null);
}

説明: 調用park函數時,首先獲取當前線程,然後設置當前線程的parkBlocker字段,即調用setBlocker函數,之後調用Unsafe類的park函數,之後再調用setBlocker函數。那麼問題來了,為什麼要在此park函數中要調用兩次setBlocker函數呢? 原因其實很簡單,調用park函數時,當前線程首先設置好parkBlocker字段,然後再調用Unsafe的park函數,此後,當前線程就已經阻塞了,等待該線程的unpark函數被調用,所以後面的一個setBlocker函數無法運行,unpark函數被調用,該線程獲得許可後,就可以繼續運行了,也就運行第二個setBlocker,把該線程的parkBlocker字段設置為null,這樣就完成了整個park函數的邏輯。如果沒有第二個setBlocker,那麼之後沒有調用park(Object blocker),而直接調用getBlocker函數,得到的還是前一個park(Object blocker)設置的blocker,顯然是不符合邏輯的。總之,必須要保證在park(Object blocker)整個函數執行完後,該線程的parkBlocker字段又恢復為null。所以,park(Object)型函數裏必須要調用setBlocker函數兩次。setBlocker方法如下。

private static void setBlocker(Thread t, Object arg) {
    // 設置線程t的parkBlocker字段的值為arg
    UNSAFE.putObject(t, parkBlockerOffset, arg);
}

説明: 此方法用於設置線程t的parkBlocker字段的值為arg。

另外一個無參重載版本,park()函數如下。

public static void park() {
    // 獲取許可,設置時間為無限長,直到可以獲取許可
    UNSAFE.park(false, 0L);
}

説明: 調用了park函數後,會禁用當前線程,除非許可可用。在以下三種情況之一發生之前,當前線程都將處於休眠狀態,即下列情況發生時,當前線程會獲取許可,可以繼續運行。

  • 其他某個線程將當前線程作為目標調用 unpark。

  • 其他某個線程中斷當前線程。

  • 該調用不合邏輯地(即毫無理由地)返回。

parkNanos函數

此函數表示在許可可用前禁用當前線程,並最多等待指定的等待時間。具體函數如下。

public static void parkNanos(Object blocker, long nanos) {
    if (nanos > 0) { // 時間大於0
        // 獲取當前線程
        Thread t = Thread.currentThread();
        // 設置Blocker
        setBlocker(t, blocker);
        // 獲取許可,並設置了時間
        UNSAFE.park(false, nanos);
        // 設置許可
        setBlocker(t, null);
    }
}

説明: 該函數也是調用了兩次setBlocker函數,nanos參數表示相對時間,表示等待多長時間。

parkUntil函數

此函數表示在指定的時限前禁用當前線程,除非許可可用, 具體函數如下:

public static void parkUntil(Object blocker, long deadline) {
    // 獲取當前線程
    Thread t = Thread.currentThread();
    // 設置Blocker
    setBlocker(t, blocker);
    UNSAFE.park(true, deadline);
    // 設置Blocker為null
    setBlocker(t, null);
}

説明: 該函數也調用了兩次setBlocker函數,deadline參數表示絕對時間,表示指定的時間。

unpark函數

此函數表示如果給定線程的許可尚不可用,則使其可用。如果線程在 park 上受阻塞,則它將解除其阻塞狀態。否則,保證下一次調用 park 不會受阻塞。如果給定線程尚未啓動,則無法保證此操作有任何效果。具體函數如下:

public static void unpark(Thread thread) {
    if (thread != null) // 線程為不空
        UNSAFE.unpark(thread); // 釋放該線程許可
}

説明: 釋放許可,指定線程可以繼續運行。

更深入的理解

與 synchronzed 的區別

synchronzed 會使線程阻塞,線程會進入 BLOCKED 狀態,而調用 LockSupprt 方法阻塞線程會使線程進入到 WAITING 狀態。

Thread.sleep()和Object.wait()的區別

首先,我們先來看看Thread.sleep()和Object.wait()的區別,這是一個爛大街的題目了,大家應該都能説上來兩點。

  • Thread.sleep()不會釋放佔有的鎖,Object.wait()會釋放佔有的鎖;

  • Thread.sleep()必須傳入時間,Object.wait()可傳可不傳,不傳表示一直阻塞下去;

  • Thread.sleep()到時間了會自動喚醒,然後繼續執行;

  • Object.wait()不帶時間的,需要另一個線程使用Object.notify()喚醒;

  • Object.wait()帶時間的,假如沒有被notify,到時間了會自動喚醒,這時又分好兩種情況,一是立即獲取到了鎖,線程自然會繼續執行;二是沒有立即獲取鎖,線程進入同步隊列等待獲取鎖;

其實,他們倆最大的區別就是Thread.sleep()不會釋放鎖資源,Object.wait()會釋放鎖資源。

Object.wait()和Condition.await()的區別

Object.wait()和Condition.await()的原理是基本一致的,不同的是Condition.await()底層是調用LockSupport.park()來實現阻塞當前線程的。

實際上,它在阻塞當前線程之前還幹了兩件事,一是把當前線程添加到條件隊列中,二是“完全”釋放鎖,也就是讓state狀態變量變為0,然後才是調用LockSupport.park()阻塞當前線程。

Thread.sleep()和LockSupport.park()的區別

LockSupport.park()還有幾個兄弟方法——parkNanos()、parkUtil()等,我們這裏説的park()方法統稱這一類方法。

  • 從功能上來説,Thread.sleep()和LockSupport.park()方法類似,都是阻塞當前線程的執行,且都不會釋放當前線程佔有的鎖資源;

  • Thread.sleep()沒法從外部喚醒,只能自己醒過來;

  • LockSupport.park()方法可以被另一個線程調用LockSupport.unpark()方法喚醒;

  • Thread.sleep()方法聲明上拋出了InterruptedException中斷異常,所以調用者需要捕獲這個異常或者再拋出;

  • LockSupport.park()方法不需要捕獲中斷異常;

  • Thread.sleep()本身就是一個native方法;

  • LockSupport.park()底層是調用的Unsafe的native方法;

Object.wait()和LockSupport.park()的區別

二者都會阻塞當前線程的運行,他們有什麼區別呢? 經過上面的分析相信你一定很清楚了,真的嗎? 往下看!

  • Object.wait()方法需要在synchronized塊中執行;

  • LockSupport.park()可以在任意地方執行;

  • Object.wait()方法聲明拋出了中斷異常,調用者需要捕獲或者再拋出;

  • LockSupport.park()不需要捕獲中斷異常;

  • Object.wait()不帶超時的,需要另一個線程執行notify()來喚醒,但不一定繼續執行後續內容;

  • LockSupport.park()不帶超時的,需要另一個線程執行unpark()來喚醒,一定會繼續執行後續內容;

park()/unpark()底層的原理是“二元信號量”,你可以把它相像成只有一個許可證的Semaphore,只不過這個信號量在重複執行unpark()的時候也不會再增加許可證,最多隻有一個許可證。

如果在wait()之前執行了notify()會怎樣?

如果當前的線程不是此對象鎖的所有者,卻調用該對象的notify()或wait()方法時拋出IllegalMonitorStateException異常;

如果當前線程是此對象鎖的所有者,wait()將一直阻塞,因為後續將沒有其它notify()喚醒它。

如果在park()之前執行了unpark()會怎樣?

線程不會被阻塞,直接跳過park(),繼續執行後續內容

LockSupport.park()會釋放鎖資源嗎?

不會,它只負責阻塞當前線程,釋放鎖資源實際上是在Condition的await()方法中實現的。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.