基本使用
// 暫停目前線程
LockSupport.park();
// 恢複某個線程的運作
LockSupport.unpark(暫停線程對象)
特點
與 Object 的 wait & notify 相比
- wait,notify 和 notifyAll 必須配合 Object Monitor 一起使用,而 park,unpark 不必
- park & unpark 是以線程為機關來【阻塞】和【喚醒】線程,而 notify 隻能随機喚醒一個等待線程,notifyAll 是喚醒所有等待線程,就不那麼【精确】
- park & unpark 可以先 unpark,而 wait & notify 不能先 notify
先Park後Unpark
package com.gzczy.concurrent.week3;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
/**
* @Description Park和Unpark
* @Author chenzhengyu
* @Date 2020-11-05 19:15
*/
@Slf4j(topic = "c.ParkAndUnParkDemo")
public class ParkAndUnParkDemo {
public static void main(String[] args) {
// demo1();
demo2();
}
public static void demo1(){
Thread t1 = new Thread(() -> {
log.debug("start...");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("park...");
LockSupport.park();
log.debug("resume...");
},"t1");
t1.start();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("unpark...");
LockSupport.unpark(t1);
}
//先Unpark再Park
public static void demo2(){
Thread t1 = new Thread(() -> {
log.debug("start...");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("park...");
LockSupport.park();
log.debug("resume...");
}, "t1");
t1.start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("unpark...");
LockSupport.unpark(t1);
}
}
源碼解析
為線程排程目的禁用目前線程,除非許可可用。
如果許可證是可用的,那麼它将被消耗,調用将立即傳回。否則,目前線程會因為線程排程的目的而被暫停,并處于休眠狀态,直到以下三種情況之一發生:
- 其他一些線程以目前線程為目标調用unpark
- 其他線程打斷目前線程
- 不符合邏輯的調用
park方法不會告知你是哪一種情況的發生而導緻的。調用者應該重新檢查導緻線程停在第一個位置的條件。調用者也可以确定,例如,線程傳回時的中斷狀态。
/**
* Disables the current thread for thread scheduling purposes unless the
* permit is available.
*
* <p>If the permit is available then it is consumed and the call returns
* immediately; otherwise
* the current thread becomes disabled for thread scheduling
* purposes and lies dormant until one of three things happens:
*
* <ul>
* <li>Some other thread invokes {@link #unpark unpark} with the
* current thread as the target; or
*
* <li>Some other thread {@linkplain Thread#interrupt interrupts}
* the current thread; or
*
* <li>The call spuriously (that is, for no reason) returns.
* </ul>
*
* <p>This method does <em>not</em> report which of these caused the
* method to return. Callers should re-check the conditions which caused
* the thread to park in the first place. Callers may also determine,
* for example, the interrupt status of the thread upon return.
*
* @param blocker the synchronization object responsible for this
* thread parking
* @since 1.6
*/
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker);
UNSAFE.park(false, 0L);
setBlocker(t, null);
}
unpark通過傳入目前線程進行取消park
/**
* Makes available the permit for the given thread, if it
* was not already available. If the thread was blocked on
* {@code park} then it will unblock. Otherwise, its next call
* to {@code park} is guaranteed not to block. This operation
* is not guaranteed to have any effect at all if the given
* thread has not been started.
*
* @param thread the thread to unpark, or {@code null}, in which case
* this operation has no effect
*/
public static void unpark(Thread thread) {
if (thread != null)
UNSAFE.unpark(thread);
}
對比Wait & Notify 優勢
LockSupport的主要作用就是讓線程進入阻塞等待和喚醒狀态
我們常見的三種線程喚醒的方法
- 方式1: 使用Object中的wait()方法讓線程等待, 使用Object中的notify()方法喚醒線程
- 方式2: 使用JUC包中Condition的await()方法讓線程等待,使用signal()方法喚醒線程
- 方式3: LockSupport類可以阻塞目前線程以及喚醒指定被阻塞的線程
Object類中的wait和notify方法實作線程等待和喚醒
package com.gzczy.concurrent.heima.b.wait;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
/**
* @Description wait notify 測試阻塞
* @Author chenzhengyu
* @Date 2021年01月31日 14:12:08
*/
@Slf4j(topic = "c.WaitNotifyDemo")
public class WaitNotifyDemo2 {
final static Object obj = new Object();
public static void main(String[] args) throws Exception {
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj) {
log.debug("執行....");
try {
obj.wait(); // 讓線程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代碼....");
}
},"t1").start();
new Thread(() -> {
synchronized (obj) {
log.debug("執行....");
obj.notify(); // 讓線程在obj上一直等待下去
log.debug("其它代碼....");
}
},"t2").start();
}
}
異常1:wait方法和notify方法,兩個都去掉同步代碼塊,抛出
IllegalMonitorStateException

異常2:将notify放在wait方法前面,程式将無法執行,無法進行喚醒
總結:wait和notify方法必須要在同步代碼塊中或者方法裡面成對進行出現才能使用,而且必須先wait後notify之後才OK
通過Park和Unpark進行實作線程等待和喚醒
package com.gzczy.concurrent.heima.b.wait;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
/**
* @Description LockSupport Demo 進行線程阻塞
* @Author chenzhengyu
* @Date 2021-01-31 14:22
*/
@Slf4j(topic = "c.LockSupportDemo")
public class LockSupportDemo {
public static void main(String[] args) throws InterruptedException {
//預設是permit 0
Thread t1 = new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("執行....是否打斷--->"+Thread.currentThread().isInterrupted());
// 調用一次park就會消費permit 由剛剛的1變為0 你繼續運作下去吧
LockSupport.park();
log.debug("其它代碼,是否打斷--->"+Thread.currentThread().isInterrupted());
//再次打住 目前沒有許可了 許可為0 那就不放你繼續運作啦
LockSupport.park();
log.debug("再次park,是否打斷--->"+Thread.currentThread().isInterrupted());
//被打斷後會發現無法park住,打斷标記已經為true 線程已經被打斷了
LockSupport.park();
}, "t1");
t1.start();
new Thread(() -> {
log.debug("執行....");
//調用一次unpark就加1變成1,線程還在運作 沒毛病老鐵 你繼續運作吧
LockSupport.unpark(t1);
log.debug("其它代碼....");
}, "t2").start();
try {
TimeUnit.SECONDS.sleep(4);
log.debug("打斷t1線程....");
t1.interrupt();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
運作結果
優勢對比
- 以前的等待喚醒通知機制必須synchronized裡面有一個wait和notify;Lock裡面有await和signal。LockSupport不用持有鎖塊,不用加鎖,程式性能好
- 先後順序,不容易導緻卡死,虛假喚醒。通過許可證進行授權
解析
每個線程都有自己的一個 Parker 對象(底層C代碼實作),由三部分組成 _counter , _cond 和 _mutex 打個比喻
- 線程就像一個旅人,Parker 就像他随身攜帶的背包,條件變量就好比背包中的帳篷。_counter 就好比背包中的備用幹糧(0 為耗盡,1 為充足)
- 調用 park 就是要看需不需要停下來歇息
-
- 如果備用幹糧耗盡,那麼鑽進帳篷歇息
- 如果備用幹糧充足,那麼不需停留,繼續前進
- 調用 unpark,就好比令幹糧充足
-
- 如果這時線程還在帳篷,就喚醒讓他繼續前進
- 如果這時線程還在運作,那麼下次他調用 park 時,僅是消耗掉備用幹糧,不需停留繼續前進
- 因為背包空間有限,多次調用 unpark 僅會補充一份備用幹糧
目前線程調用UnSafe.park
- 檢查 _counter ,本情況為 0,這時,獲得 _mutex 互斥鎖
- 線程進入 _cond 條件變量阻塞
- 設定 _counter = 0
目前線程調用UnSafe.unpark(許可為0)
- 調用 Unsafe.unpark(Thread_0) 方法,設定 _counter 為 1
- 喚醒 _cond 條件變量中的 Thread_0
- Thread_0 恢複運作
- 設定 _counter 為 0
目前線程調用UnSafe.unpark(許可為1)
1. 調用 Unsafe.unpark(Thread_0) 方法,設定 _counter 為 1
2. 目前線程調用 Unsafe.park() 方法
3. 檢查 _counter ,本情況為 1,這時線程無需阻塞,繼續運作
4. 設定 _counter 為 0
總結
LockSupport是用來建立鎖和其他同步類的基本線程阻塞原語,它使用了一種名為Permit(許可)的概念來做到阻塞和喚醒線程的功能,每個線程都有一個許可(Permit),Permit隻有兩個值:1和0,預設是0。我們可以把許可看成是一種信号量(Semaphore),但是與Semaphore不同的是,許可的累加上限是1
LockSupport是一個線程阻塞工具,所有的方法都是靜态方法,可以讓線程在任意位置進行阻塞。阻塞之後也有對應的喚醒方法,歸根結底,LockSupport調用的是UnSafe裡面的native代碼
LockSupport提供了Park和Unpark的方法實作阻塞線程和解除線程阻塞的過程
LockSupport和每個使用它的線程都有一個許可(Permit)進行關聯,Permit相當于1,0的開關,預設是0
調用一次unpark就加1變成1,調用一次park就會消費permit,也就是将1變成0的這個過程,同時将park立即傳回
如果再次調用park會變成阻塞(因為Permit為0,是以會阻塞在這裡,直至Permit變為1),如果這個時候調用unpark會把permit設定為1。每個線程都有一個相關的Permit,Permit最多隻有1個,重複調用unpark也不會積累憑證
面試題
為什麼可以先喚醒線程後阻塞線程?
答:因為UnPark獲得了一個憑證,之後調用Park方法,就可以名正言順的憑證消費,故不會阻塞
為什麼喚醒兩次後阻塞兩次,但是最終的結果還是會阻塞線程?
答:因為憑證的數量最多為1,連續調用兩次unpark和調用一次unpark的效果是一樣的,隻會增加一個憑證;而調用兩次park卻需要消費兩個憑證,證不夠,不能放行