LockSupport

N1ce2cu發表於2024-07-27

LockSupprot 用來阻塞和喚醒執行緒,底層實現依賴於 Unsafe 類(後面會細講)。

該類包含一組用於阻塞和喚醒執行緒的靜態方法,這些方法主要是圍繞 park 和 unpark 展開。

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

        // 當 counterThread 數到 10 時,它會喚醒 mainThread。而 mainThread 在呼叫 park 方法時會被阻塞,直到被 unpark。
        Thread counterThread = new Thread(() -> {
            for (int i = 1; i <= 20; i++) {
                System.out.println(i);
                if (i == 10) {
                    // 當數到10時,喚醒主執行緒
                    LockSupport.unpark(mainThread);
                }
            }
        });
        counterThread.start();

        // 主執行緒呼叫park
        LockSupport.park();
        System.out.println("Main thread was unparked.");
    }
}

阻塞執行緒


  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() 方法實現的。

Dump 執行緒


"Dump 執行緒"通常是指獲取執行緒的當前狀態和呼叫堆疊的詳細快照。這可以提供關於執行緒正在執行什麼操作以及執行緒在程式碼的哪個部分的重要資訊。

下面是執行緒轉儲中可能包括的一些資訊:

  • 執行緒 ID 和名稱:執行緒的唯一識別符號和可讀名稱。
  • 執行緒狀態:執行緒的當前狀態,例如執行(RUNNABLE)、等待(WAITING)、睡眠(TIMED_WAITING)或阻塞(BLOCKED)。
  • 呼叫堆疊:執行緒的呼叫堆疊跟蹤,顯示執行緒從當前執行點回溯到初始呼叫的完整方法呼叫序列。
  • 鎖資訊:如果執行緒正在等待或持有鎖,執行緒轉儲通常還包括有關這些鎖的資訊。

執行緒轉儲可以透過各種方式獲得,例如使用 Java 的 jstack 工具,或從 Java VisualVM、Java Mission Control 等工具獲取。

下面是一個簡單的例子,透過 LockSupport 阻塞執行緒,然後透過 Intellij IDEA 檢視 dump 執行緒資訊。

public class LockSupportDemo {
    public static void main(String[] args) {
        LockSupport.park();
    }
}

先執行程式,再在 Run 皮膚中找到 attach to process,選擇 attach 到主執行緒:

再到 debugger 皮膚中找到 export threads。

匯出後就能看見執行緒資訊了。

與 synchronized 的區別


synchronized 會使執行緒阻塞,執行緒會進入 BLOCKED 狀態,而呼叫 LockSupprt 方法阻塞執行緒會使執行緒進入到 WAITING 狀態。

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            System.out.println("Thread is parked now");
            LockSupport.park();
            System.out.println("Thread is unparked now");
        });
        thread.start();

        try {
            Thread.sleep(3000); // 主執行緒等待3秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        LockSupport.unpark(thread); // 主執行緒喚醒阻塞的執行緒
    }
}

設計思路


LockSupport 會為使用它的執行緒關聯一個許可證(permit)狀態,permit 的語義「是否擁有許可」,0 代表否,1 代表是,預設是 0。

  • LockSupport.unpark:指定執行緒關聯的 permit 直接更新為 1,如果更新前的permit<1,喚醒指定執行緒
  • LockSupport.park:當前執行緒關聯的 permit 如果>0,直接把 permit 更新為 0,否則阻塞當前執行緒

  • 執行緒 A 執行LockSupport.park,發現 permit 為 0,未持有許可證,阻塞執行緒 A
  • 執行緒 B 執行LockSupport.unpark(入參執行緒 A),為 A 執行緒設定許可證,permit 更新為 1,喚醒執行緒 A
  • 執行緒 B 流程結束
  • 執行緒 A 被喚醒,發現 permit 為 1,消費許可證,permit 更新為 0
  • 執行緒 A 執行臨界區
  • 執行緒 A 流程結束

經過上面的分析得出結論 unpark 的語義明確為「使執行緒持有許可證」,park 的語義明確為「消費執行緒持有的許可」,所以 unpark 與 park 的執行順序沒有強制要求,只要控制好使用的執行緒即可,unpark=>park執行流程如下:

  • permit 預設是 0,執行緒 A 執行 LockSupport.unpark,permit 更新為 1,執行緒 A 持有許可證
  • 執行緒 A 執行 LockSupport.park,此時 permit 是 1,消費許可證,permit 更新為 0
  • 執行臨界區
  • 流程結束

因 park 阻塞的執行緒不僅僅會被 unpark 喚醒,還可能會被執行緒中斷(Thread.interrupt)喚醒,而且不會丟擲 InterruptedException 異常,所以建議在 park 後自行判斷執行緒中斷狀態,來做對應的業務處理。

為什麼推薦使用 LockSupport 來做執行緒的阻塞與喚醒(執行緒間協同工作),因為它具備如下優點:

  • 以執行緒為操作物件更符合阻塞執行緒的直觀語義
  • 操作更精準,可以準確地喚醒某一個執行緒(notify 隨機喚醒一個執行緒,notifyAll 喚醒所有等待的執行緒)
  • 無需競爭鎖物件(以執行緒作為操作物件),不會因競爭鎖物件產生死鎖問題
  • unpark 與 park 沒有嚴格的執行順序,不會因執行順序引起死鎖問題,比如「Thread.suspend 和 Thread.resume」沒按照嚴格順序執行,就會產生死鎖

面試題


有 3 個獨立的執行緒,一個只會輸出 A,一個只會輸出 B,一個只會輸出 C,在三個執行緒啟動的情況下,請用合理的方式讓他們按順序列印 ABCABC。

public class Main {
    private static Thread t1, t2, t3;

    public static void main(String[] args) {
        t1 = new Thread(() -> {
            for (int i = 0; i < 2; i++) {
                LockSupport.park();
                System.out.print("A");
                LockSupport.unpark(t2);
            }
        });

        t2 = new Thread(() -> {
            for (int i = 0; i < 2; i++) {
                LockSupport.park();
                System.out.print("B");
                LockSupport.unpark(t3);
            }
        });

        t3 = new Thread(() -> {
            for (int i = 0; i < 2; i++) {
                LockSupport.park();
                System.out.print("C");
                LockSupport.unpark(t1);
            }
        });

        t1.start();
        t2.start();
        t3.start();

        // 主執行緒稍微等待一下,確保其他執行緒已經啟動並且進入park狀態。
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 啟動整個流程
        LockSupport.unpark(t1);
    }
}

LockSupport 提供了一種更底層和靈活的執行緒排程方式。它不依賴於同步塊或特定的鎖物件。可以用於構建更復雜的同步結構,例如自定義鎖或併發容器。LockSupport.park 與 LockSupport.unpark 的組合使得執行緒之間的精確控制變得更容易,而不需要複雜的同步邏輯和物件監視。