Java多線程進階(七)—— J.U.C之locks框架:AQS獨佔功能剖析(2)

1dbf61a8f03ff931e3858e44c968f795.jpg

本文首發於一世流雲的專欄: https://segmentfault.com/blog...

1、本章概述

本章以ReentrantLock的調用爲例,說明AbstractQueuedSynchronizer提供的獨佔功能。
本章結構以下:java

  1. 以ReentrantLock的公平策略爲例,分析AbstractQueuedSynchronizer的獨佔功能
  2. 以ReentrantLock的非公平策略爲例,分析AbstractQueuedSynchronizer的獨佔功能
  3. 分析AbstractQueuedSynchronizer的鎖中斷、限時等待等功能

2、ReentrantLock的公平策略原理

本節對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

2.1 ThreadA首先獲取到鎖

ThreadA首先調用ReentrantLock的lock方法,咱們看下該方法的內部:
clipboard.png併發

最終其實調用了FairSync的lock方法:
clipboard.pngui

acquire方法來自AQS:
clipboard.pngspa

其中tryAcquire方法須要AQS的子類本身去實現,咱們來看下ReentrantLock中的實現:
clipboard.png線程

能夠看到,在ReentrantLock中,同步狀態State的含義以下:設計

State 資源的定義
0 表示鎖可用
1 表示鎖被佔用
大於1 表示鎖被佔用,且值表示同一線程的重入次數

ThreadA是首個獲取鎖的線程,因此上述方法會返回true,第一階段結束。(ThreadA一直保持佔有鎖的狀態)
此時,AQS中的等待隊列仍是空:
clipboard.png3d

2.2 ThreadB開始獲取鎖

終於,ThreadB要登場了,同樣,ThreadB先去調用lock方法,最終調用AQS的acquire方法:
clipboard.pngcode

tryAcquire方法確定是返回false(由於此時ThreadA佔有着鎖)。
接下來看下addWaiter方法,這個方法其實就是將當前調用線程包裝成一個【獨佔結點】,添加到等待隊列尾部。
clipboard.png
clipboard.png

這裏關鍵是enq方法,由於併發插入的狀況存在,因此該方法設計成了自旋操做,保證結點能成功插入,具體步驟以下:
①當隊列爲空的時候,先建立一個dummy頭結點;
clipboard.png

②進入下一次循環,插入隊尾結點。
clipboard.png

好了,ThreadB已經被包裝成結點插入隊尾了,接下來會調用acquireQueued方法,這也是AQS中最重要的方法之一:
clipboard.png

在AQS中,等待隊列中的線程都是阻塞的,當某個線程被喚醒時,只有該線程是首結點(線程)時,纔有權去嘗試獲取鎖。

上述方法中,將ThreadB包裝成結點插入隊尾後,先判斷ThreadB是不是首結點(注意不是頭結點,頭結點是個dummy結點),發現確實是首結點(node.predecessor==head),因而調用tryAcquire嘗試獲取鎖,可是獲取失敗了(此時ThreadA佔有着鎖),就要判斷是否須要阻塞當前線程。

判斷是否須要阻塞線程:
clipboard.png

注意,對於獨佔功能,只使用了3種結點狀態:

結點狀態 描述
CANCELLED 1 取消。表示後驅結點被中斷或超時,須要移出隊列
SIGNAL -1 發信號。表示後驅結點被阻塞了(當前結點在入隊後、阻塞前,應確保將其prev結點類型改成SIGNAL,以便prev結點取消或釋放時將當前結點喚醒。)
CONDITION -2 Condition專用。表示當前結點在Condition隊列中,由於等待某個條件而被阻塞了

對於在等待隊列中的線程,若是要阻塞它,須要確保未來有線程能夠喚醒它,AQS中經過將前驅結點的狀態置爲SIGNAL:-1來表示未來會喚醒當前線程,當前線程能夠安心的阻塞。

看下圖或許比較好理解:
①插入完ThreadB後,隊列的初始狀態以下:
clipboard.png

②雖然ThreadB是隊首結點,可是它拿不到鎖(被ThreadA佔有着),因此ThreadB會阻塞,但在阻塞前須要設置下前驅的狀態,以便未來能夠喚醒我:
clipboard.png

至此,ThreadB的執行也暫告一段落了(安心得在等待隊列中睡覺)。

注意:補充一點,若是ThreadB在阻塞過程當中被中斷,實際上是不會拋出異常的,只會在acquireQueued方法返回時,告訴調用者在阻塞器件有沒被中斷過,具體若是處理,要不要拋出異常,取決於調用者,這實際上是一種延時中斷機制。
clipboard.png

2.3 ThreadC開始獲取鎖

終於輪到ThreadC出場了,ThreadC的調用過程和ThreadB徹底同樣,一樣拿不到鎖,而後加入到等待隊列隊尾:
clipboard.png

而後,ThreadC在阻塞前須要把前驅結點的狀態置爲SIGNAL:-1,以確保未來能夠被喚醒:
clipboard.png

至此,ThreadC的執行也暫告一段落了(安心得在等待隊列中睡覺)。

2.4 ThreadA釋放鎖

ThreadA終於使用完了臨界資源,要釋放鎖了,來看下ReentrantLock的unlock方法:
clipboard.png

unlock內部調用了AQS的release方法,傳參1:
clipboard.png

嘗試釋放鎖的操做tryRelease
clipboard.png

釋放成功後,調用unparkSuccessor方法,喚醒隊列中的首結點:
clipboard.png

此時,隊列狀態爲:
clipboard.png

2.5 ThreadB喚醒後繼續執行

好了,隊首結點(ThreadB)被喚醒了。
ThreadB會繼續從如下位置開始執行,先返回一箇中斷標識,用於表示ThreadB在阻塞期間有沒被中斷過:
clipboard.png

而後ThreadB又開始了自旋操做,被喚醒的是隊首結點,因此能夠嘗試tryAcquire獲取鎖,此時獲取成功(ThreadA已經釋放了鎖)。
獲取成功後會調用setHead方法,將頭結點置爲當前結點,並清除線程信息:
clipboard.png
clipboard.png

最終的隊列狀態以下:
clipboard.png

2.6 ThreadB釋放鎖

ThreadB也終於使用完了臨界資源,要釋放鎖了,過程和ThreadA釋放時同樣,釋放成功後,會調用unparkSuccessor方法,喚醒隊列中的首結點:
clipboard.png

隊首結點(ThreadC)被喚醒後,繼續從原來的阻塞處向下執行,並嘗試獲取鎖,獲取成功,最終隊列狀態以下:
clipboard.png

2.7 ThreadC釋放鎖

ThreadC也終於使用完了臨界資源,要釋放鎖了。釋放成功後,調用unparkSuccessor方法,喚醒隊列中的首結點:
此時隊列中只剩下一個頭結點(dummy),因此這個方法其實什麼都不作。最終隊列的狀態就是隻有一個dummy頭結點。
clipboard.png

至此,AQS的獨佔功能已經差很少分析完了,剩下還有幾個內容沒分析:

  1. 鎖中斷功能
  2. 限時等待功能
  3. Conditon等待功能

這些功能將在後續章節陸續分析。

3、ReentrantLock的非公平策略原理

ReenrantLock非公平策略的內部實現和公平策略沒啥太大區別:
非公平策略和公平策略的最主要區別在於:

  1. 公平鎖獲取鎖時,會判斷等待隊列中是否有線程排在當前線程前面。只有沒有狀況下,纔去獲取鎖,這是公平的含義。
    clipboard.png
  2. 非公平鎖獲取鎖時,會當即嘗試修改同步狀態,失敗後再調用AQS的acquire方法。
    clipboard.png
    acquire方法會轉調非公平鎖自身的tryAcquire方法,其實最終是調了nofairTryAcquire方法,而該方法相對於公平鎖,只是少了「隊列中是否有其它線程排在當前線程前」這一判斷:
    clipboard.png

4、AQS對中斷的支持

仍是以ReentrantLock爲例,來看下AQS是如何實現鎖中斷和超時的。
咱們知道ReentrantLock的lockInterruptibly方法是會響應中斷的。(線程若是在阻塞過程當中被中斷,會拋出InterruptedException異常)

該方法調用了AQS的acquireInterruptibly方法:
clipboard.png

上述代碼會先去嘗試獲取鎖,若是失敗,則調用doAcquireInterruptibly方法,以下:
clipboard.png

很眼熟有木有?看下和acquireQueued方法的對比,惟一的區別就是:
當調用線程獲取鎖失敗,進入阻塞後,若是中途被中斷,acquireQueued只是用一個標識記錄線程被中斷過,而doAcquireInterruptibly則是直接拋出異常。
clipboard.png

5、AQS對限時等待的支持

Lock接口中有一個方法:tryLock,用於在指定的時間內嘗試獲取鎖,獲取不到就返回。
ReentrantLock實現了該方法,能夠看到,該方法內部調用了AQS的tryAcquireNanos方法:
clipboard.png

tryAcquireNanos方法是響應中斷的,先嚐試獲取一次鎖,失敗則調用doAcquireNanos方法進行超時等待:
clipboard.png

關鍵是doAcquireNano方法,和acquireQuqued方法相似,又是一個自旋操做,在超時前不斷嘗試獲取鎖,獲取不到則阻塞(加上了等待時間的判斷)。該方法內部,調用了LockSupport.parkNanos來超時阻塞線程:
clipboard.png

LockSupport.parkNanos內部其實經過Unsafe這個類來操做線程的阻塞,底層是一個native方法:
clipboard.png

若是當前線程在指定時間內獲取不到鎖,除了返回false外,最終還會執行cancelAcquire方法:
clipboard.png


示例

爲了便於理解仍是以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通過兩輪自旋操做後,等待隊列的狀況以下:
clipboard.png

2. ThreadB先到超時時間
調用了cancelAcquire方法取消操做,隊列狀態變成:
clipboard.png

3. ThreadC到達超時時間
調用了cancelAcquire方法取消操做,隊列狀態變成:
clipboard.png

在退出cancelAcquire後,原來ThreadB和ThreadC對應的結點會被JVM垃圾回收器回收。

6、總結

本章從ReentrantLock入手,分析AQS的獨佔功能的內部實現細節。下一章,從CountDownLatch入手,看下AQS的共享功能如何實現。

相關文章
相關標籤/搜索