JUC在深入面試題——三種方式實現執行緒等待和喚醒(wait/notify,await/signal,LockSupport的park/unpark)

2022-09-23 12:01:16

一、前言

在多執行緒的場景下,我們會經常使用加鎖,來保證執行緒安全。如果鎖用的不好,就會陷入死鎖,我們以前可以使用Objectwait/notify來解決死鎖問題。也可以使用Conditionawait/signal來解決,當然最優還是LockSupportpark/unpark。他們都是解決執行緒等待和喚醒的。下面來說說具體的優缺點和例子證明一下。

二、wait/notify的使用

1. 程式碼演示

public class JUC {

    static Object lock = new Object();

    public static void main(String[] args) {
        new Thread(()->{
            synchronized (lock) {// 1
                System.out.println(Thread.currentThread().getName() + "進來");
                try {
                    // 釋放鎖,陷入阻塞,直到有人喚醒
                    lock.wait();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }// 1
            System.out.println(Thread.currentThread().getName() + "我被喚醒了");
        }, "A").start();

        new Thread(()->{
            synchronized (lock) {// 2
                lock.notify();
                System.out.println(Thread.currentThread().getName() + "隨機喚醒一個執行緒");
            }// 2
        }, "B").start();
    }
}

2. 執行結果

3. 測試不在程式碼塊執行(把上面程式碼註釋1給刪除

4. 修改程式碼

try {
    TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
    e.printStackTrace();
}

5. 總結

wait和notify方法必須要在同步塊或者方法裡面且成對出現使用,否則會丟擲java.lang.IllegalMonitorStateException

呼叫順序要先wait後notify才可以正常阻塞和喚醒。

三、await/signal的使用

1. 程式碼演示

public class JUC {

    static ReentrantLock reentrantLock = new ReentrantLock();
    static Condition condition = reentrantLock.newCondition();

    public static void main(String[] args) {
        new Thread(()->{
            reentrantLock.lock();// 1
            try {
                System.out.println(Thread.currentThread().getName()+"進來");
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                reentrantLock.unlock();// 1
            }

            System.out.println(Thread.currentThread().getName()+"我被喚醒了");
        },"A").start();

        new Thread(()->{
            reentrantLock.lock();// 1
            try {
                condition.signal();
                System.out.println(Thread.currentThread().getName()+"隨機喚醒一個執行緒");
            }finally {
                reentrantLock.unlock();// 1
            }
        },"B").start();

    }
}

2. 執行結果

3. 測試不在程式碼塊執行(把上面程式碼註釋1給刪除

4. 修改程式碼

try {
    TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
    e.printStackTrace();
}

5. 總結

await和signal方法必須要在同步塊或者方法裡面且成對出現使用,否則會丟擲java.lang.IllegalMonitorStateException

呼叫順序要先await後signal才可以正常阻塞和喚醒。——和wait/notify一致

四、LockSupport的park/unpark的使用

1. LockSupport介紹

LockSupport是用來建立鎖和其他同步類的基本執行緒阻塞原語

LockSupport類使用了一種名為Permit(許可)的概念來做到阻塞和喚醒執行緒的功能,每個執行緒都有一個許可(permit),permit只有兩個值1和0,預設是0。

可以把許可看成是一種(0、1)號誌(Semaphore),但與Semaphore不同的是,許可的累加上限是1

2. park原始碼檢視

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

作用:park()/park(Object blocker) - 阻塞當前執行緒阻塞傳入的具體執行緒

我們會發現底層是呼叫sun.misc.Unsafe:這個類的提供了一些繞開JVM的更底層功能,基於它的實現可以提高效率。

permit預設是0,所以一開始呼叫park()方法,當前執行緒就會阻塞,直到別的執行緒將當前執行緒的permit設定為1時park方法會被喚醒,然後會將permit再次設定為0並返回。

3. unpark原始碼檢視

public static void unpark(Thread thread) {
    if (thread != null)
        UNSAFE.unpark(thread);
}

作用:unpark(Thread thread) - 喚醒處於阻塞狀態的指定執行緒
我們會發現底層都是呼叫sun.misc.Unsafe
呼叫unpark(thread)方法後,就會將thread執行緒的許可permit設定成1注意多次呼叫unpark方法,不會累加,pemit值還是1)會自動喚醒thead執行緒,即之前阻塞中的LockSupport.park()方法會立即返回。

4. 程式碼演示

public class JUC {

    public static void main(String[] args) {

        Thread a = new Thread(()->{
            System.out.println(Thread.currentThread().getName() + "進來");
            LockSupport.park();
            System.out.println(Thread.currentThread().getName() + " 被換醒了");
        }, "A");
        a.start();

        Thread b = new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            LockSupport.unpark(a);
            System.out.println(Thread.currentThread().getName()+"喚醒傳入的執行緒");
        }, "B");
        b.start();

    }
}

5. 結果展示

6. 修改程式碼

try {
	TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
	e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "進來" + System.currentTimeMillis());
LockSupport.park();
System.out.println(Thread.currentThread().getName() + " 被換醒了" + System.currentTimeMillis());

7. 與前兩者比的優點

park/unpark不需要在同步塊或者方法內才能執行,解決了上面兩種不在同步塊或者方法就報錯的情況。

park/unpark不需要先執行park,在執行unpark,無需在意順序。解決了上面兩種必須有前後順序的情況。

8.總結

LockSupport是用來建立鎖和共他同步類的基本執行緒阻塞原語

LockSuport是一個執行緒阻塞工具類,所有的方法都是靜態方法,可以讓執行緒在任意位置阻塞,阻寨之後也有對應的喚醒方法。歸根結底,LockSupport呼叫的Unsafe中的native程式碼(C++)。

public native void park(boolean var1, long var2);

LockSupport提供park()和unpark()方法實現阻塞執行緒和解除執行緒阻塞的過程。

LockSupport和每個使用它的執行緒都有一個許可(permit)關聯。permit相當於1,0的開關,預設是0,呼叫一次unpark就加1變成1,呼叫一次park會消費permit,也就是將1變成0,同時park立即返回。

再次呼叫park會變成阻塞(因為permit為零了會阻塞在這裡,一直到permit變為1),這時呼叫unpark會把permit置為1。每個執行緒都有一個相關的permit,permit最多隻有一個重複呼叫unpark也不會積累憑證


阻塞原因:根據上面程式碼,我們會先執行執行緒B,呼叫unpark方法,雖然進行兩次unpark。但是只有一個有效,此時permit為1。此時A執行緒開始,來到第一個park,permit消耗後為0,為0是阻塞等待unpark,此時沒有unpark了,所以一直陷入阻塞

9.白話文理解

執行緒阻塞需要消耗憑證(permit),這個憑證最多隻有1個。
當呼叫park方法時
如果有憑證,則會直接消耗掉這個憑證然後正常退出。
如果無憑證,就必須阻塞等待憑證可用。
而unpark則相反,它會增加一個憑證,但憑證最多隻能有1個,累加無放。

五、面試題

為什麼可以先喚醒執行緒後阻塞執行緒?

因為unpark獲得了一個憑證,之後再呼叫park方法,此時permit為1,就可以名正言順的憑證消費,permit為0,故不會阻塞。

為什麼喚醒兩次後阻塞兩次,但最終結果還會阻塞執行緒?

因為憑證的數量最多為1(不能累加),連續呼叫兩次 unpark和呼叫一次 unpark效果一樣,只會增加一個憑證;而呼叫兩次park卻需要消費兩個憑證,證不夠,不能放行。

六、總結

看到這裡的小夥伴,點個贊不過分吧,小編也是整理了一下午,參考陽哥課件。


歡迎大家關注小編的微信公眾號!!

推廣自己網站時間到了!!!

點選存取!歡迎存取,裡面也是有很多好的文章哦!