Java併發編程(二):LockSupport應用及原理分析

1 引言

上一篇簡單介紹了Java中的Thread機制,附傳送門↓:
Java併發編程(一):瞭解Thread

我們知道,可以通過wait()讓線程等待,通過notify()喚醒線程,但使用wait(),notify()來實現等待喚醒功能至少有兩個缺點:

  1. wait()notify()都是Object中的方法,在調用這兩個方法前必須先獲得鎖對象,這限制了其使用場合:只能在同步代碼塊中。

  2. 另一個缺點是當對象的等待隊列中有多個線程時,notify()只能隨機選擇一個線程喚醒,無法喚醒指定的線程

而使用LockSupport的話,我們可以在任何場合使線程阻塞,同時也可以指定要喚醒的線程,較爲方便。

2 LockSupport的應用

首先列舉LockSupport常用的幾個方法:

// 暫停當前線程
public static void park(Object blocker); 
// 暫停當前線程,不過有超時時間的限制
public static void parkNanos(Object blocker, long nanos); 
// 暫停當前線程,直到某個時間
public static void parkUntil(Object blocker, long deadline); 
 // 無期限暫停當前線程
public static void park();
 // 暫停當前線程,不過有超時時間的限制
public static void parkNanos(long nanos);
 // 暫停當前線程,直到某個時間
public static void parkUntil(long deadline);
 // 恢復指定線程的運行
public static void unpark(Thread thread);
public static Object getBlocker(Thread t);

方法入參的Object blocker是代表什麼呢?這其實就是方便在線程dump的時候看到具體的阻塞對象的信息

再通過一個小栗子簡單演示LockSupport的用法:

/**
 * @author Carson Chu, [email protected]
 * @date 2020/4/4 10:41
 * @description
 */
public class Main {
    public static void main(String[] args) {
        FutureTask<Boolean> futureTask = new FutureTask<>(() -> {
            for (int i = 0; i < 2; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
            }
            return true;
        });
        Thread thread1 = new Thread(futureTask, "thread-1");
        Thread thread2 = new Thread(() -> {
            for (int i = 2; i < 4; i++) {
                System.out.println(Thread.currentThread().getName() + ":" + i);
                /* 讓線程等待 */
                LockSupport.park();
            }
        }, "thread-2");
        thread1.start();
        thread2.start();
        LockSupport.unpart(thread2);
    }
}

由上述代碼可以看出,線程2在調度的時候,進入循環第一次輸出數字時,調用的LockSupport.park()方法,所以線程2只會輸出一個數字然後進入等待狀態。當然最後不要忘記調用LockSupport.unpark()喚醒。

在瞭解LockSupport的等待喚醒的基本操作之後,再來看下面一段代碼:

public class Main {
    public static void main(String[] args) {
        FutureTask<Boolean> futureTask = new FutureTask<>(() -> {
            Thread currentThread = Thread.currentThread();
            for (int i = 0; i < 2; i++) {
                System.out.println(currentThread.getName() + ":" + i);
                /* 喚醒線程 */
                LockSupport.unpark(currentThread);
            }
            /* 讓線程等待 */
            LockSupport.park();
            return true;
        });
        Thread thread1 = new Thread(futureTask, "thread-1");
        thread1.start();
    }
}

上述代碼關鍵在於喚醒操作執行在了等待操作之前,那麼線程是否會發生死鎖呢?

答案是不會,這點不同於stopresume機制,stopresume如果順序反了,會出現死鎖現象。

那麼是什麼原因呢?

讓我們來結合源碼分析。
首先貼出park()的實現原理:

public static void park() {
        /* 該方法是本地方法native */
        UNSAFE.park(false, 0L);
    }

再貼出unpark()的實現原理:

public static void unpark(Thread thread) {
        if (thread != null)
            /* 該方法是本地方法native */
            UNSAFE.unpark(thread);
    }

觀察上述源碼,尤其是park(),眼光銳利如你,相信一定看到了false這個關鍵字,沒錯,這就是重點。實際上LockSupport底層維護了一個許可(permit),即上述源碼裏的布爾值。那麼它是什麼原理呢?一言以蔽之就是:

park()調用時,判斷許可是否爲true,如果是true,則繼續往下執行;如果是false,則調用線程等待,直到許可爲true。當unpark()調用時,如果當前線程還未進入park(),則許可爲true。

再結合上述樣例代碼,我先調用unpark()方法,此時線程還有進入park(),所以許可爲true,之後再調用park()方法時,判斷許可值爲true,則線程不會執行等待操作,而是繼續執行,因而不會發生死鎖。

小結

  1. park()unpark()可以實現類似wait()notify()的功能,但是並不和wait()notify()交叉,也就是說unpark()不會對wait()起作用,notify()也不會對park()起作用。
  2. park()unpark()的使用不會出現死鎖的情況。

點點關注,不會迷路

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