JDK14的AbstractQueuedSynchronizer(AQS)源碼解析

介紹

基於JDK14的源碼進行解析,需要看過源碼後,再來理解本文會簡單很多。

doc文檔說明

AQS類提供了框架來實現阻塞鎖和相關的同步器FIFO等待隊列,用AtomicInteger代替state,繼承該類必須保護好state,state代表的是獲取鎖(acquire)或釋放鎖(release)。其他的方法都是入隊列和阻塞機制。

Node結點有三種狀態:

WAITING   = 1;          // must be 1
CANCELLED = 0x80000000; // must be negative
COND      = 2;          // in a condition wait

處於head結點的,是持有資源的線程。

方法

JDK14流程
enqueue方法:for循環 + cas的方式,入隊列。
isEnqueued方法:從鏈表尾部查找node,若有返回true。

獲取排他鎖流程

acquire方法:先調用tryAcquire獲取鎖,若返回false,則調用acquire(私有)方法。

acquire(私有)方法:
重複幹如下事情:

  • 檢查當前的前繼結點是否是頭結點,如果是,確保頭結點穩定,否則確保有效的前繼結點
  • 如果前繼結點是頭結點,或尚未加入隊列,則嘗試獲取鎖,若獲取鎖成功則結束
  • 如果結點尚未創建,則創建它;否則,如果結點尚未入隊,請嘗試一次入隊;否則從park中喚醒,重試(到postSpin時間);否則,如果等待狀態未設置,則設置並重試;否則暫停當前並清除等待狀態,並檢查取消;

若超時,則調用cancelAcquire取消當前結點。

不是
不是
成功
失敗
不是
爲空
不爲空
不是
不是
不是
不是
沒有
超時
被unpark
獲取鎖流程
檢查是否該結點的前繼結點是否是頭結點
檢查前繼結點狀態是否小於0
調用cleanQueue方法清空小於0的結點
檢查前繼結點的前繼結點是否爲null
讓CPU自旋一會
前置結點爲null或前置結點爲head結點
嘗試獲取鎖
持有該資源
若結點爲空則創建結點
前置結點爲null
cas入隊列
前置結點爲頭結點且已經再次獲取資源失敗
若當前結點的狀態爲0
設置狀態爲WAITING
是否設置過超時時間
線程park
是否超時
調用cancelAcquire取消當前結點
設置狀態爲0

cancelAcquire方法:設置當前node狀態爲WAITING狀態,調用cleanQueue方法。
cleanQueue方法:清空所有狀態爲CANCELLED的node結點,並調用signalNext方法嘗試unpark下一個結點。
signalNext方法:喚醒該結點的後繼結點,若後繼結點狀態不等於0,則unpark,它從acquire(私有)方法裏面執行清空等待狀態然後重複去申請一次資源看是否能成功。不用擔心後繼結點爲CANCELLED狀態,因爲後續發現這個狀態會調用cleanQueue方法清空所有狀態爲CANCELLED的node結點。而且CANCELLED狀態只有線程持有state時候,纔回去調用cleanQueue,所以不用考慮併發。

釋放排它鎖流程

release方法:調用自定義的tryRelease釋放資源,若爲true,則調用signalNext方法喚醒該結點的後繼結點。

獲取共享鎖流程

和排它鎖類似,只要tryAcquireShared(arg) >= 0就能拿到共享鎖。
signalNextIfShared方法:signalNext方法類似,只是多了個判斷是否是共享鎖對象的條件。

ConditionObject

該類需要在AQS中使用,類似於wait,sleep,該類維護着一個單向鏈表ConditionNode。只用了排它鎖。

await方法

創建一個ConditionNode結點,若當前結點的等待線程是該鎖,設置狀態爲00000011(COND | WAITING),添加到鏈表中,釋放當前鎖。若當前線程已經有結點進入等待隊列了,等待隊列中結點釋放,然後清空狀態爲0,入等待隊列。

singal方法

遍歷ConditionObject鏈表,若遍歷時對應的結點的狀態是3,即00000011(代表着該結點已經處於等待狀態),通過cas方式入等待隊列。

jdk實現類

CountDownLatch(共享鎖)

構造函數中先設置初始狀態爲入參。

  • 獲取鎖的時候,state減一,直到爲0。
  • 釋放鎖的時候,判斷state是否爲0,爲0才允許釋放。

使用場景例子: 一個任務的某個步驟需要很多小任務,主任務就相當於主線程,分出來各個小任務給其他線程,就可以使用CountDownLatch等待所有的小任務都執行完畢,主任務再往下執行。

Semaphore(共享鎖)

構造函數中先設置初始狀態爲入參。有着公平鎖和非公平鎖實現類。
公平鎖:直接先去競爭state,競爭失敗入隊列。
非公平鎖:如果有前驅結點在隊列中,直接入隊列。

使用場景:Semaphore可以用於做流量控制,特別公用資源有限的應用場景,比如數據庫連接。假如有一個需求,要讀取幾萬個文件的數據,因爲都是IO密集型任務,我們可以啓動幾十個線程併發的讀取,但是如果讀到內存後,還需要存儲到數據庫中,而數據庫的連接數只有10個,這時我們必須控制只有十個線程同時獲取數據庫連接保存數據,否則會報錯無法獲取數據庫連接。這個時候,我們就可以使用Semaphore來做流控。

ThreadPoolExecutor.Worker(排它鎖)

一個Worker線程對應一個等待隊列。

ReentrantLock(排它鎖)

前置條件,可重入鎖,若發現持有者和當前線程一致,state++,然後執行公平鎖和非公平鎖的邏輯。
公平鎖:直接先去競爭state,競爭失敗入隊列。
非公平鎖:如果有前驅結點在隊列中,直接入隊列。

ReentrantReadWriteLock(排它鎖和共享鎖)

公平鎖:直接先去競爭state,競爭失敗入隊列。
非公平鎖:如果有前驅結點在隊列中,直接入隊列。
使用的是同一個等待隊列,共用一個state。

WriteLock(排它鎖、可重入)

獲取鎖流程:

  • 如果state不爲0
    • state和65535與運算等於0,進入等待隊列。
    • 當前線程和持有線程不一致,進入等待隊列。
    • 如果state + acquires大於65535,拋出異常。
    • 以上都沒有,state設置爲state + acquires,繼續持有該鎖。
  • 否則說明當前沒有資源的競爭
    • 如果是非公平鎖,直接獲取鎖,若成功持有鎖,若失敗則進入等待隊列。
    • 如果是公平鎖且有前驅結點在隊列中,直接入隊列;否則持有鎖。

釋放鎖流程,和前面大體一致。

ReadLock(共享鎖)

維護着一個HoldCounter的ThreadLocal,用於記錄每個線程所重入的次數。
獲取鎖流程:

  • 如果有WriteLock鎖且持有鎖者與當前線程不一致,則進入等待隊列。
  • 調用~readerShouldBlock方法,
    • 如果是非公平鎖且head的後繼結點不爲共享鎖,繼續流程。
    • 如果是公平鎖且有沒有前驅結點在隊列中,繼續流程。
    • HoldCounter++,如果是第一個讀線程,記錄該結點。
    • 繼續持有鎖,結束流程。
  • 如果有WriteLock鎖且持有鎖者與當前線程不一致,則進入等待隊列。
  • 調用readerShouldBlock方法
    • 如果是非公平鎖且head的後繼結點不爲共享鎖,繼續流程。
    • 如果是公平鎖且有沒有前驅結點在隊列中,繼續流程。
    • 如果第一個讀線程是當前線程,繼續流程。
    • 如果當前讀線程所重入次數==0,進入等待隊列,結束流程。
  • HoldCounter++,如果是第一個讀線程,記錄該結點。

釋放鎖流程,HoldCounter–,如果是第一個讀線程,清除該結點,等待state == 0時,纔會釋放所有讀線程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章