我們知道,concurrent包是基於AQS (AbstractQueuedSynchronizer)框架,AQS框架借助於兩個類:Unsafe(提供CAS操作) 和 LockSupport(提供park/unpark操作)。因此,LockSupport可謂構建concurrent包的基礎之一。理解concurrent包,就從這里開始。
LockSupport 簡介
LockSupport
是一個線程阻塞工具類,所有的方法都是靜態方法,可以讓線程在任意位置阻塞,當然阻塞之后肯定得有喚醒的方法。歸根結底,LockSupport調用的Unsafe中的native代碼。
LockSupport是用來創建鎖和其他同步類的基本線程阻塞原語。LockSupport 提供park()和unpark()方法實現阻塞線程和解除線程阻塞,LockSupport和每個使用它的線程都有一個許可(permit)關聯。permit相當於1,0的開關,默認是0,調用一次unpark就加1變成1,調用一次park會消費permit, 也就是將1變成0,同時park立即返回。再次調用park會變成block(因為permit為0了,會阻塞在這里,直到permit變為1), 這時調用unpark會把permit置為1。每個線程都有一個相關的permit, permit最多只有一個,重復調用unpark也不會積累。
park() 和 unpark()不會有 Thread.suspend
和 Thread.resume
所可能引發的死鎖問題,由於許可的存在,調用 park 的線程和另一個試圖將其 unpark 的線程之間的競爭將保持活性。
如果調用線程被中斷,則park方法會返回。同時park也擁有可以設置超時時間的版本。
public static void park(Object blocker); // 暫停當前線程 public static void parkNanos(Object blocker, long nanos); // 暫停當前線程,不過有超時時間的限制 public static void parkUntil(Object blocker, long deadline); // 暫停當前線程,直到某個時間 public static void park(); // 無期限暫停當前線程 public static void parkNanos(long nanos); // 暫停當前線程,不過有超時時間的限制 public static void parkUntil(long deadline); // 暫停當前線程,直到某個時間 public static void unpark(Thread thread); // 恢復當前線程 public static Object getBlocker(Thread t);
為什么叫park呢,park英文意思為停車。我們如果把Thread看成一輛車的話,park就是讓車停下,unpark就是讓車啟動然后跑起來。
我們可以使用它來阻塞和喚醒線程,功能和wait,notify有些相似,但是LockSupport比起wait,notify功能更強大,也好用的多。
示例
1、使用 wait,notify 阻塞喚醒線程
@Test public void testWaitNotify() { Object obj = new Object(); Thread waitThread = new Thread(() -> { synchronized (obj) { System.out.println("start wait!!!"); try {obj.wait();} catch (InterruptedException e) {e.printStackTrace();} System.out.println("end wait!!!"); } }); Thread notifyThread = new Thread(() -> { synchronized (obj) { System.out.println("start notify"); obj.notify(); System.out.println("end notify!!!"); } }); waitThread.start(); notifyThread.start(); }
結果如下所示:
start wait!!! start notify end notify!!! end wait!!!
使用 wait,notify 來實現等待喚醒功能至少有兩個缺點:
- 1. 由上面的例子可知,wait 和 notify 都是 Object 中的方法,在調用這兩個方法前必須先獲得鎖對象,這限制了其使用場合:只能在同步代碼塊中。
- 2. 另一個缺點可能上面的例子不太明顯,當對象的等待隊列中有多個線程時,notify只能隨機選擇一個線程喚醒,無法喚醒指定的線程。
而使用LockSupport的話,我們可以在任何場合使線程阻塞,同時也可以指定要喚醒的線程,相當的方便。
2、使用 LockSupport 阻塞喚醒線程
@Test public void testLockSupport() { Thread parkThread = new Thread(() -> { System.out.println("開始線程阻塞"); LockSupport.park(); System.out.println("結束線程阻塞"); }); parkThread.start(); System.out.println("開始線程喚醒"); LockSupport.unpark(parkThread); System.out.println("結束線程喚醒"); }
結果如下所示:
開始線程阻塞 開始線程喚醒 結束線程阻塞 結束線程喚醒
LockSupport.park();
可以用來阻塞當前線程,park是停車的意思,把運行的線程比作行駛的車輛,線程阻塞則相當於汽車停車,相當直觀。
該方法還有個變體 LockSupport.park(Object blocker),
指定線程阻塞的對象 blocker,該對象主要用來排查問題。方法 LockSupport.unpark(Thread thread)
用來喚醒線程,因為需要線程作參數,所以可以指定線程進行喚醒。
許可
上面的這個“許可”是不能疊加的,“許可”是一次性的。
比如線程B 連續調用了三次 unpark函數,當線程A 調用 park函數就使用掉這個“許可”,如果線程A 再次調用 park,則進入等待狀態。
注意,unpark函數 可以先於 park 調用。比如線程B 調用 unpark函數,給線程A 發了一個“許可”,那么當線程A 調用 park 時,它發現已經有“許可”了,那么它會馬上再繼續運行。
可能有些朋友還是不理解“許可”這個概念,我們深入HotSpot的源碼來看看。每個java線程都有一個Parker實例,Parker類是這樣定義的:
class Parker : public os::PlatformParker { private: volatile int _counter ; ... public: void park(bool isAbsolute, jlong time); void unpark(); ... } class PlatformParker : public CHeapObj<mtInternal> { protected: pthread_mutex_t _mutex [1] ; pthread_cond_t _cond [1] ; ... }
LockSupport就是通過控制變量 _counter
來對線程阻塞喚醒進行控制的。原理有點類似於信號量機制。
- 當調用
park()
方法時,會將 _counter 置為 0,同時判斷前值 < 1 說明前面被unpark
過,則直接退出,否則將使該線程阻塞。 - 當調用
unpark()
方法時,會將 _counter 置為 1,同時判斷前值 < 1 會進行線程喚醒,否則直接退出。
形象的理解,線程阻塞需要消耗憑證(permit),這個憑證最多只有1個。當調用 park方法時,如果有憑證,則會直接消耗掉這個憑證然后正常退出;但是如果沒有憑證,就必須阻塞等待憑證可用;而 unpark則相反,它會增加一個憑證,但憑證最多只能有1個。 - 為什么可以先喚醒線程后阻塞線程?
因為 unpark獲得了一個憑證,之后調用 park因為有憑證消費,故不會阻塞。 - 為什么喚醒兩次后阻塞兩次會阻塞線程。
因為憑證的數量最多為 1,連續調用兩次 unpark 和 調用一次 unpark 效果一樣,只會增加一個憑證;而調用兩次 park卻需要消費兩個憑證。
總結
- 面向的主體不一樣。LockSuport 主要是針對 Thread 進行阻塞處理,可以指定阻塞隊列的目標對象,每次可以指定具體的線程喚醒。Object.wait() 是以對象為緯度,阻塞當前的線程和喚醒單個或所有線程。
- 實現機制不同。兩者的阻塞隊列並不交叉。也就是說
unpark
不會對wait
起作用,notify
也不會對park
起作用。object.notifyAll() 不能喚醒 LockSupport 的阻塞 Thread。