Java併發包原始碼學習系列:掛起與喚醒執行緒LockSupport工具類

天喬巴夏丶發表於2021-01-17

系列傳送門:

LockSupport概述

LockSupport工具類定義了一組公共的靜態方法,提供了最基本的執行緒阻塞和喚醒功能,是建立鎖和其他同步類的基礎,你會發現,AQS中阻塞執行緒和喚醒執行緒的地方,就是使用LockSupport提供的park和unpark方法,比如下面這段:

    // 掛起執行緒
	private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }
	// 喚醒執行緒
    private void unparkSuccessor(Node node) {
		//...
        if (s != null)
            LockSupport.unpark(s.thread);
    }

park與unpark相關方法

LockSupport提供了一組park開頭的方法來阻塞當前執行緒【省略static】:

  • void park():阻塞當前執行緒,如果呼叫unpark(Thread thread)方法或者當前執行緒被中斷,才能從park()方法返回。
  • void parkNanos(long nanos):阻塞當前執行緒,最長不超過nanos納秒,返回條件在park()的基礎上增加了超時返回。
  • void parkUntil(long deadline):阻塞當前執行緒,直到deadline【從1970年開始到deadline時間的毫秒數】時間。
  • void unpark(Thread thread):喚醒處於阻塞狀態的執行緒thread。

JDK1.6中,增加了帶有blocker引數的幾個方法,blocker引數用來標識當前執行緒在等待的物件,用於問題排查和系統監控。

下面演示park()方法和unpark()方法的使用:

在thread執行緒中呼叫park()方法,預設情況下該執行緒是不持有許可證的,因此將會被阻塞掛起。

unpark(thread)方法將會讓thread執行緒獲得許可證,才能從park()方法返回。

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(() ->{
            String name = Thread.currentThread().getName();
            System.out.println(name + " begin park");
            LockSupport.park();// 如果呼叫park的執行緒已經獲得了關聯的許可證,就會立即返回
            System.out.println(name + " end park");
        },"A");
        thread.start(); // 預設情況下,thread不持有許可證,會被阻塞掛起

        Thread.sleep(1000); 

        System.out.println(thread.getName() + " begin unpark");

        LockSupport.unpark(thread);//讓thread獲得許可證

    }
// 結果如下
A begin park
A begin unpark
A end park

你需要理解,許可證在這裡的作用,我們也可以事先給執行緒一個許可證,接著在park的時候就不會被阻塞了。

    public static void main(String[] args) {
        System.out.println("begin park");
        // 使當前執行緒獲得許可證
        LockSupport.unpark(Thread.currentThread());
        // 再次呼叫park方法,因為已經有許可證了,不會被阻塞
        LockSupport.park();
        System.out.println("end park");
    }
// 結果如下
begin park
end park

中斷演示

執行緒被中斷的時候,park方法不會丟擲異常,因此需要park退出之後,對中斷狀態進行處理。

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            String name = Thread.currentThread().getName();
            System.out.println(name + " begin park");
            // 一直掛起自己,只有被中斷,才會推出迴圈
            while (!Thread.currentThread().isInterrupted()) {
                LockSupport.park();
            }
            System.out.println(name + " end park");
        }, "A");
        thread.start();
        Thread.sleep(1000);
        System.out.println("主執行緒準備中斷執行緒" + thread.getName());
        // 中斷thread
        thread.interrupt();
    }

// 結果如下
A begin park
主執行緒準備中斷執行緒A
A end park

blocker的作用

JDK1.6開始,一系列park方法開始支援傳入blocker引數,標識當前執行緒在等待的物件,當執行緒在沒有持有許可證的情況下呼叫park方法而被阻塞掛起時,這個blocker物件會被記錄到該執行緒內部。

    public static void park(Object blocker) {
        Thread t = Thread.currentThread();
        setBlocker(t, blocker); // 設定blocker
        UNSAFE.park(false, 0L);
        setBlocker(t, null); // 清除blocker
    }

Thread類裡有個volatile Object parkBlocker變數,用來存放park方法傳遞的blocker物件,也就是把blocker變數存放到了呼叫park方法的執行緒的成員變數中。

接下來我們通過兩個例子感受一下:

測試無blocker

public class TestParkWithoutBlocker {
    public void park(){
        LockSupport.park();
    }

    public static void main(String[] args) throws InterruptedException {
        new TestParkWithoutBlocker().park();
        Thread.sleep(3000);
    }
}

使用jps命令,列出當前執行的程式4412 TestPark,接著使用jstack 4412命令檢視執行緒堆疊:

測試帶blocker

public class TestBlockerPark {

    public void park(){
        LockSupport.park(this); // 傳入blocker = this
    }

    public static void main(String[] args) throws InterruptedException {
        new TestBlockerPark().park();
        Thread.sleep(3000);
    }
}

明顯的差別就在於,使用帶blocker 引數的park方法,能夠通過jstack看到具體阻塞物件的資訊:

- parking to wait for  <0x000000076b77dff0> (a chapter6_1_LockSupport.TestBlockerPark)

診斷工具可以呼叫getBlocker(Thread)方法來獲取blocker物件,JDK推薦我們使用帶有blocker引數的park方法,並且設定blocker為this,這樣當在列印執行緒堆疊排查問題的時候就能夠知道那個類被阻塞了。

JDK提供的demo

老傳統了,摘一段JavaDoc上的使用案例:

/**
 * 先進先出的鎖,只有佇列的首元素可以獲取鎖
 */
class FIFOMutex {
    private final AtomicBoolean locked = new AtomicBoolean(false);
    private final Queue<Thread> waiters
            = new ConcurrentLinkedQueue<Thread>();

    public void lock() {
        // 中斷標誌
        boolean wasInterrupted = false; 
        Thread current = Thread.currentThread();
        waiters.add(current);

        // 不是隊首執行緒 或 當前鎖已經被其他執行緒獲取,則呼叫park方法掛起自己
        while (waiters.peek() != current ||
                !locked.compareAndSet(false, true)) {
            LockSupport.park(this);
            // 如果park方法是因為被中斷而返回,則忽略中斷,並且重置中斷標誌
            // 接著再次進入迴圈
            if (Thread.interrupted()) // ignore interrupts while waiting
                wasInterrupted = true;
        }
        
        waiters.remove();
        // 如果標記為true,則中斷執行緒
        // [雖然我對中斷訊號不感興趣,忽略它,但是不代表其他執行緒對該標誌不感興趣,因此恢復一下.]
        if (wasInterrupted)          // reassert interrupt status on exit
            current.interrupt();
    }

    public void unlock() {
        locked.set(false);
        LockSupport.unpark(waiters.peek());
    }
}

總結

  • LockSupport提供了有關執行緒掛起park和喚醒unpark的靜態方法。
  • JDK1.6之後允許傳入blocker阻塞物件,便於問題監控和排查。
  • 如果park的執行緒被中斷,不會丟擲異常,需要自行對中斷狀態進行處理。

參考閱讀

  • 翟陸續 薛冰田 《Java併發程式設計之美》
  • 方騰飛 《Java併發程式設計的藝術》
  • 【J.U.C】LockSupport

相關文章