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

一、前言

在多線程的場景下,我們會經常使用加鎖,來保證線程安全。如果鎖用的不好,就會陷入死鎖,我們以前可以使用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卻需要消費兩個憑證,證不夠,不能放行。

六、總結

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


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

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

點擊訪問!歡迎訪問,裏面也是有很多好的文章哦!

Tags: