在多執行緒的場景下,我們會經常使用加鎖,來保證執行緒安全。如果鎖用的不好,就會陷入死鎖,我們以前可以使用Object
的wait/notify
來解決死鎖問題。也可以使用Condition
的await/signal
來解決,當然最優還是LockSupport
的park/unpark
。他們都是解決執行緒等待和喚醒的。下面來說說具體的優缺點和例子證明一下。
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();
}
}
把上面程式碼註釋1給刪除
)try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
wait和notify方法必須要在同步塊或者方法
裡面且成對出現使用
,否則會丟擲java.lang.IllegalMonitorStateException
。
呼叫順序要先wait後notify才可以正常阻塞和喚醒。
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();
}
}
把上面程式碼註釋1給刪除
)try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
await和signal方法必須要在同步塊或者方法
裡面且成對出現使用
,否則會丟擲java.lang.IllegalMonitorStateException
。
呼叫順序要先await後signal才可以正常阻塞和喚醒。——和wait/notify一致
。
LockSupport是用來建立鎖和其他同步類的基本執行緒阻塞原語
。
LockSupport類使用了一種名為Permit(許可)的概念來做到阻塞和喚醒執行緒
的功能,每個執行緒都有一個許可(permit)
,permit只有兩個值1和0
,預設是0。
可以把許可看成是一種(0、1)號誌(Semaphore),但與Semaphore不同的是,許可的累加上限是1
。
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並返回。
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()方法會立即返回。
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();
}
}
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());
park/unpark不需要在同步塊或者方法內
才能執行,解決了上面兩種不在同步塊或者方法就報錯的情況。
park/unpark不需要先執行park,在執行unpark,無需在意順序
。解決了上面兩種必須有前後順序的情況。
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
了,所以一直陷入阻塞
。
執行緒阻塞需要消耗憑證(permit),這個憑證最多隻有1個。
當呼叫park方法時
如果有憑證,則會直接消耗掉這個憑證然後正常退出。
如果無憑證,就必須阻塞等待憑證可用。
而unpark則相反,它會增加一個憑證,但憑證最多隻能有1個,累加無放。
為什麼可以先喚醒執行緒後阻塞執行緒?
因為unpark獲得了一個憑證,之後再呼叫park方法,此時permit為1,就可以名正言順的憑證消費,permit為0,故不會阻塞。
為什麼喚醒兩次後阻塞兩次,但最終結果還會阻塞執行緒?
因為憑證的數量最多為1(不能累加),連續呼叫兩次 unpark和呼叫一次 unpark效果一樣,只會增加一個憑證;而呼叫兩次park卻需要消費兩個憑證,證不夠,不能放行。
看到這裡的小夥伴,點個贊不過分吧,小編也是整理了一下午,參考陽哥課件。
歡迎大家關注小編的微信公眾號!!
推廣自己網站時間到了!!!