本文首發於一世流雲的專欄: https://segmentfault.com/blog...
本章以ReentrantLock的調用爲例,說明AbstractQueuedSynchronizer提供的獨佔功能。
本章結構以下:java
本節對ReentrantLock公平策略的分析基於如下示例:node
假設如今有3個線程:ThreadA、ThreadB、ThreadC,一個公平的獨佔鎖,3個線程會依次嘗試去獲取鎖:
ReentrantLock lock=new ReentrantLock(true);
線程的操做時序以下:segmentfault
//ThreadA lock //ThreadB lock //ThreadC lock //ThreadA release //ThreadB release //ThreadC release
ThreadA首先調用ReentrantLock的lock方法,咱們看下該方法的內部:併發
最終其實調用了FairSync的lock方法:ui
acquire方法來自AQS:spa
其中tryAcquire方法須要AQS的子類本身去實現,咱們來看下ReentrantLock中的實現:線程
能夠看到,在ReentrantLock中,同步狀態State的含義以下:設計
State | 資源的定義 |
---|---|
0 | 表示鎖可用 |
1 | 表示鎖被佔用 |
大於1 | 表示鎖被佔用,且值表示同一線程的重入次數 |
ThreadA是首個獲取鎖的線程,因此上述方法會返回true,第一階段結束。(ThreadA一直保持佔有鎖的狀態)
此時,AQS中的等待隊列仍是空:3d
終於,ThreadB要登場了,同樣,ThreadB先去調用lock方法,最終調用AQS的acquire方法:code
tryAcquire方法確定是返回false(由於此時ThreadA佔有着鎖)。
接下來看下addWaiter方法,這個方法其實就是將當前調用線程包裝成一個【獨佔結點】,添加到等待隊列尾部。
這裏關鍵是enq方法,由於併發插入的狀況存在,因此該方法設計成了自旋操做,保證結點能成功插入,具體步驟以下:
①當隊列爲空的時候,先建立一個dummy頭結點;
②進入下一次循環,插入隊尾結點。
好了,ThreadB已經被包裝成結點插入隊尾了,接下來會調用acquireQueued方法,這也是AQS中最重要的方法之一:
在AQS中,等待隊列中的線程都是阻塞的,當某個線程被喚醒時,只有該線程是首結點(線程)時,纔有權去嘗試獲取鎖。
上述方法中,將ThreadB包裝成結點插入隊尾後,先判斷ThreadB是不是首結點(注意不是頭結點,頭結點是個dummy結點),發現確實是首結點(node.predecessor==head),因而調用tryAcquire嘗試獲取鎖,可是獲取失敗了(此時ThreadA佔有着鎖),就要判斷是否須要阻塞當前線程。
判斷是否須要阻塞線程:
注意,對於獨佔功能,只使用了3種結點狀態:
結點狀態 | 值 | 描述 |
---|---|---|
CANCELLED | 1 | 取消。表示後驅結點被中斷或超時,須要移出隊列 |
SIGNAL | -1 | 發信號。表示後驅結點被阻塞了(當前結點在入隊後、阻塞前,應確保將其prev結點類型改成SIGNAL,以便prev結點取消或釋放時將當前結點喚醒。) |
CONDITION | -2 | Condition專用。表示當前結點在Condition隊列中,由於等待某個條件而被阻塞了 |
對於在等待隊列中的線程,若是要阻塞它,須要確保未來有線程能夠喚醒它,AQS中經過將前驅結點的狀態置爲SIGNAL:-1來表示未來會喚醒當前線程,當前線程能夠安心的阻塞。
看下圖或許比較好理解:
①插入完ThreadB後,隊列的初始狀態以下:
②雖然ThreadB是隊首結點,可是它拿不到鎖(被ThreadA佔有着),因此ThreadB會阻塞,但在阻塞前須要設置下前驅的狀態,以便未來能夠喚醒我:
至此,ThreadB的執行也暫告一段落了(安心得在等待隊列中睡覺)。
注意:補充一點,若是ThreadB在阻塞過程當中被中斷,實際上是不會拋出異常的,只會在acquireQueued方法返回時,告訴調用者在阻塞器件有沒被中斷過,具體若是處理,要不要拋出異常,取決於調用者,這實際上是一種延時中斷機制。
![]()
終於輪到ThreadC出場了,ThreadC的調用過程和ThreadB徹底同樣,一樣拿不到鎖,而後加入到等待隊列隊尾:
而後,ThreadC在阻塞前須要把前驅結點的狀態置爲SIGNAL:-1,以確保未來能夠被喚醒:
至此,ThreadC的執行也暫告一段落了(安心得在等待隊列中睡覺)。
ThreadA終於使用完了臨界資源,要釋放鎖了,來看下ReentrantLock的unlock方法:
unlock內部調用了AQS的release方法,傳參1:
嘗試釋放鎖的操做tryRelease:
釋放成功後,調用unparkSuccessor方法,喚醒隊列中的首結點:
此時,隊列狀態爲:
好了,隊首結點(ThreadB)被喚醒了。
ThreadB會繼續從如下位置開始執行,先返回一箇中斷標識,用於表示ThreadB在阻塞期間有沒被中斷過:
而後ThreadB又開始了自旋操做,被喚醒的是隊首結點,因此能夠嘗試tryAcquire獲取鎖,此時獲取成功(ThreadA已經釋放了鎖)。
獲取成功後會調用setHead方法,將頭結點置爲當前結點,並清除線程信息:
最終的隊列狀態以下:
ThreadB也終於使用完了臨界資源,要釋放鎖了,過程和ThreadA釋放時同樣,釋放成功後,會調用unparkSuccessor方法,喚醒隊列中的首結點:
隊首結點(ThreadC)被喚醒後,繼續從原來的阻塞處向下執行,並嘗試獲取鎖,獲取成功,最終隊列狀態以下:
ThreadC也終於使用完了臨界資源,要釋放鎖了。釋放成功後,調用unparkSuccessor方法,喚醒隊列中的首結點:
此時隊列中只剩下一個頭結點(dummy),因此這個方法其實什麼都不作。最終隊列的狀態就是隻有一個dummy頭結點。
至此,AQS的獨佔功能已經差很少分析完了,剩下還有幾個內容沒分析:
這些功能將在後續章節陸續分析。
ReenrantLock非公平策略的內部實現和公平策略沒啥太大區別:
非公平策略和公平策略的最主要區別在於:
仍是以ReentrantLock爲例,來看下AQS是如何實現鎖中斷和超時的。
咱們知道ReentrantLock的lockInterruptibly方法是會響應中斷的。(線程若是在阻塞過程當中被中斷,會拋出InterruptedException異常)
該方法調用了AQS的acquireInterruptibly方法:
上述代碼會先去嘗試獲取鎖,若是失敗,則調用doAcquireInterruptibly方法,以下:
很眼熟有木有?看下和acquireQueued方法的對比,惟一的區別就是:
當調用線程獲取鎖失敗,進入阻塞後,若是中途被中斷,acquireQueued只是用一個標識記錄線程被中斷過,而doAcquireInterruptibly則是直接拋出異常。
Lock接口中有一個方法:tryLock,用於在指定的時間內嘗試獲取鎖,獲取不到就返回。
ReentrantLock實現了該方法,能夠看到,該方法內部調用了AQS的tryAcquireNanos方法:
tryAcquireNanos方法是響應中斷的,先嚐試獲取一次鎖,失敗則調用doAcquireNanos方法進行超時等待:
關鍵是doAcquireNano方法,和acquireQuqued方法相似,又是一個自旋操做,在超時前不斷嘗試獲取鎖,獲取不到則阻塞(加上了等待時間的判斷)。該方法內部,調用了LockSupport.parkNanos
來超時阻塞線程:
LockSupport.parkNanos
內部其實經過Unsafe這個類來操做線程的阻塞,底層是一個native方法:
若是當前線程在指定時間內獲取不到鎖,除了返回false外,最終還會執行cancelAcquire方法:
爲了便於理解仍是以3個線程爲例:
假設如今有3個線程:ThreadA、ThreadB、ThreadC,一個公平的獨佔鎖,3個線程會依次嘗試去獲取鎖,不過此時加上了限時等待:ThreadB等待10s,ThreadA等待20s。
ReentrantLock lock=new ReentrantLock(true); //ThreadA tryLock //ThreadB tryLock, 10s //ThreadC tryLock, 20s //ThreadA release //ThreadB release //ThreadC release
1. ThreadA首先獲取到鎖,ThreadB和ThreadC依次嘗試去獲取鎖
ThreadB和ThreadC通過兩輪自旋操做後,等待隊列的狀況以下:
2. ThreadB先到超時時間
調用了cancelAcquire方法取消操做,隊列狀態變成:
3. ThreadC到達超時時間
調用了cancelAcquire方法取消操做,隊列狀態變成:
在退出cancelAcquire後,原來ThreadB和ThreadC對應的結點會被JVM垃圾回收器回收。
本章從ReentrantLock入手,分析AQS的獨佔功能的內部實現細節。下一章,從CountDownLatch入手,看下AQS的共享功能如何實現。