JUC在深入面試題——三種方式實現線程等待和喚醒(wait/notify,await/signal,LockSupport的park/unpark)
一、前言
在多線程的場景下,我們會經常使用加鎖,來保證線程安全。如果鎖用的不好,就會陷入死鎖,我們以前可以使用Object
的wait/notify
來解決死鎖問題。也可以使用Condition
的await/signal
來解決,當然最優還是LockSupport
的park/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卻需要消費兩個憑證,證不夠,不能放行。
六、總結
看到這裡的小夥伴,點個贊不過分吧,小編也是整理了一下午,參考陽哥課件。
歡迎大家關注小編的微信公眾號!!
推廣自己網站時間到了!!!