AQS 架構之 LockSupport 線程阻塞工具類,aqslocksupport
■ 前言
並發包一直是 JDK 裡面比較難理解的,同時也是很精美的語言,膜拜下 Doug Li 大神。作者不敢長篇大論,只求循序漸進地把並發包通過理論和實戰 (代碼) 的方式介紹給大家。
其實做每一件事都是挺難的,不過只要下筆就不會瞻前顧後。謝謝大家的鼓勵協助,感謝我的好基友KIRA~ 好的,熱身先從 LockSupport 開始吧~
■ LockSupport 綜述
- 定義: LockSupport 是一個線程阻塞工具類,可用於線上程內任意位置讓線程阻塞和釋放
- 作用: LockSupport 通常不會被直接使用,更多是作為鎖實現的基礎工具類
- 實現: LockSupport 底層依賴UnSafe實現,即 park() 和 unpark() 原語方法,通過"許可"替代狀態
- 使用: park方法用於線程等待"許可",unpark方法用於為線程提供"許可"
- 補充1:由於"許可"的存在,當出現一個線程調用park方法,其他線程調用unpark方法時,會保持活躍
- 補充2:若開JVM篇的話筆者會從JVM源碼角度再次解析park和unpark的底層實現,其實質用mutex和condition維護一個_counter(park->0,unpark->1)的變數,即"許可"是一次性的
- 補充3:此番為 AQS 架構之綜述 (趕製中) 的子番
■ LockSupport 資料結構
1. 類定義
public class LockSupport
2. 構造器
//私人構造器,不能被執行個體化 -- 實質就是個工作類,只能調用靜態方法private LockSupport() {} // Cannot be instantiated.
3. UnSafe
// Hotspot implementation via intrinsics APIprivate static final sun.misc.Unsafe UNSAFE;//用於記錄線程被誰阻塞的,用於線程監控和分析工具來定位原因,其表示parkBlocker在記憶體的位移量//之所以用位移量是因為parkBlockerOffset被賦值時線程必須是阻塞的,阻塞時直接調方法無效只能走記憶體private static final long parkBlockerOffset;private static final long SEED;private static final long PROBE;private static final long SECONDARY;static { try { UNSAFE = sun.misc.Unsafe.getUnsafe(); Class<?> tk = Thread.class; //擷取指定變數的記憶體位移量 parkBlockerOffset = UNSAFE.objectFieldOffset (tk.getDeclaredField("parkBlocker")); SEED = UNSAFE.objectFieldOffset (tk.getDeclaredField("threadLocalRandomSeed")); PROBE = UNSAFE.objectFieldOffset (tk.getDeclaredField("threadLocalRandomProbe")); SECONDARY = UNSAFE.objectFieldOffset (tk.getDeclaredField("threadLocalRandomSecondarySeed")); } catch (Exception ex) { throw new Error(ex); }}
4. permit (許可)
- LockSupport 和每個使用它的線程都與一個permit關聯,某種意義上可認為是 Semaphore 類,但區別於Semaphores,permit至多隻有一個,並不能被累加(即重複調動unpark也不會累加,最多為1)
- permit 相當於一個開關(只有0和1兩個值),預設為0,執行過程如下:
- 調用unpark方法,permit+1,即permit=1
- 調用park方法,permit被消費-1,即permit=0,同時park方法理解返回
- 再次調用park方法,線程會被阻塞(此時permit=0,線程無許可可用,直到permit=1之前都會被阻塞)
■ LockSupport 資料結構
1. setBlocker / getBlocker
1 /** 2 * Returns the blocker object supplied to the most recent 3 * invocation of a park method that has not yet unblocked, or null 4 * if not blocked. The value returned is just a momentary 5 * snapshot -- the thread may have since unblocked or blocked on a 6 * different blocker object. 7 * 返回提供給最近一次尚未解除阻塞的被park方法調用的blocker對象,如果該調用未阻塞,則返回null 8 * @param t the thread 9 * @return the blocker10 * @throws NullPointerException if argument is null11 * @since 1.612 */13 public static Object getBlocker(Thread t) {14 if (t == null)15 throw new NullPointerException();16 return UNSAFE.getObjectVolatile(t, parkBlockerOffset);17 }18 /**19 * This object is recorded while the thread is blocked to permit monitoring and diagnostic 20 * tools to identify the reasons that threads are blocked.21 * 此對象線上程受阻塞時被記錄,以允許監視工具和診斷工具確定線程受阻塞的原因22 */23 private static void setBlocker(Thread t, Object arg) {24 // Even though volatile, hotspot doesn't need a write barrier here.25 UNSAFE.putObject(t, parkBlockerOffset, arg);26 }
- 通過線程 Dump 查看一下,可以很明顯的看到阻塞的堆棧資訊,但其實資訊是差不多一樣的
· 2. park
- 作用:該方法用於等待"許可",調用時可能發生以下兩種情況:
- 當"許可"可用時,立即返回並且消費這個許可(將許可變成不可用)
- 當"許可"不可用時,當前線程可能被阻塞 java.lang.Thread.State : WAITING parking
- 使用: 由於park方法可能在任何時候"無理由"返回,因此通常會在迴圈中使用(在返回之前再次檢查條件)
- 適用: park方法是"busy wait"(忙碌等待)的一種最佳化 (即不需要在自旋上浪費太多時間),但它必須與 unpark 配對使用才更高效
- 注意: park方法的許可預設是被佔用的,在unpark之前調用會擷取不到許可而被阻塞
public static void park() { UNSAFE.park(false, 0L);}//納秒級逾時返回public static void parkNanos(long nanos) { if (nanos > 0) UNSAFE.park(false, nanos);}//毫秒級限時等待//注意這裡的時間需要使用系統時間加上需要等待的時間//LockSupport.parkUntil(System.currentTimeMillis() + 3000);public static void parkUntil(long deadline) { UNSAFE.park(true, deadline);}//三種形式的 park 還各自支援一個 blocker 對象參數//建議最好使用這些形式,而不是不帶此參數的原始形式//在鎖實現中提供的作為 blocker 的普通參數是 thispublic static void park(Object blocker) { Thread t = Thread.currentThread(); setBlocker(t, blocker); UNSAFE.park(false, 0L); setBlocker(t, null);}public static void parkNanos(Object blocker, long nanos) { if (nanos > 0) { Thread t = Thread.currentThread(); setBlocker(t, blocker); UNSAFE.park(false, nanos); setBlocker(t, null); }}public static void parkUntil(Object blocker, long deadline) { Thread t = Thread.currentThread(); setBlocker(t, blocker); UNSAFE.park(true, deadline); setBlocker(t, null);}
· 3. unpark
- 作用: 該方法用於提供"許可",會將還停用"許可"變成可用
- 注意: 由於 park方法預設是許可佔有並阻塞線程,因此調用 park之前最好先調用 unpark (當然因為 park\unpark 的順序解耦性,所以前後執行順序無所謂,只是代碼上最好遵循 先釋放再擷取 的規則)
/** * 注意:必須指定一個線程(但無所謂該線程是否park),將嘗試釋放其可能擁有的許可 */public static void unpark(Thread thread) { if (thread != null) UNSAFE.unpark(thread);}
· 4. LockSupport 不可重新進入
- 不可重新進入: LockSupport 不可重新進入,當一個線程多次調用 park 方法,線程將被第二個 park 方法阻塞
1 public static void main(String[] args) { 2 LockSupport.unpark(Thread.currentThread());//我們直接用主線程 3 System.out.println("執行unpark"); 4 LockSupport.park(); 5 System.out.println("執行第一次park"); 6 LockSupport.park(); 7 System.out.println("執行第二次park"); 8 while (true); 9 }10 ---------------------11 //輸出:12 執行unpark13 執行第一次park14 //分析:通過列印結果可以發現第7行其實並沒有被列印,根據下面的圖片可以看到線程在第7行(對應圖片的第16行)上阻塞
- 使用 jstack 命令查看一下線程狀態,會發現線程是 WAITING 狀態,即等待阻塞,而且還是被 park方法阻塞
·5. LockSupport 與中斷
- 中斷響應: LockSupport支援中斷響應,線程調用park阻塞時仍能夠響應插斷要求,但不會拋出InterruptedException異常
public static void main(String[] args) { Thread thread = new Thread(() -> { long start = System.currentTimeMillis(); while ((System.currentTimeMillis() - start) <= 1000);//空轉1s System.out.println("空轉1s結束"); LockSupport.park();//等待"許可" System.out.println(Thread.currentThread().getName() + "是否被中斷:" + Thread.currentThread().isInterrupted()); },"kira"); thread.start(); thread.interrupt();//中斷線程}---------------------//輸出:空轉1s結束kira是否被中斷:true//分析:通過先中斷線程再park可以發現可擷取中斷響應,同時並沒有拋出任何異常
■ suspend() VS wait() VS park()
1. suspend() VS wait()
- suspend() 不會釋放鎖,wait()會釋放鎖同時還支援逾時處理
2. suspend() VS wait()
- LockSupport 解決了suspend()不釋放鎖從而容易死結的問題,比如resume()方法被阻塞時,即其他線程在調用 resume()方法之前擷取同步鎖時被阻塞而導致 resume()方法無法執行進而導致死結
3. park() VS wait()
- LockSupport 不需要先獲得某個對象的鎖,也不會排除 InterruptedException異常
- unpark 方法可以先於park方法調用,其沒有方法調用的時序問題
- wait/notify 機制有個問題在於線程調用notify方法去喚醒其他線程時,需要保證需被喚醒線程必須被wait方法阻塞,否則被喚醒線程會永遠處於 WAITING 狀態,同時notify方法只能喚醒一個線程,當同時有多個線程在同一個對象上 wait 等待,就只能有一個線程可以被喚醒(不能指定)
- park/unpark 機制通過引入單個"許可"的概念實現對線程同步的解耦,線程間無須關心對方的狀態,因為不需要一個變數專門用於儲存狀態