Java併發包源碼學習系列:掛起與喚醒線程LockSupport工具類

系列傳送門:

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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章