AQS應用之Lock詳解

寫在前面

前面對synchronized進行了一個詳細的介紹,包括對鎖的升級和最佳化都進行了詳細的總結,出來synchronized,還有lock可以在多執行緒併發下,保證資料的原子性,那麼lock是如何來實現的?這就要從AQS講起,話不多說

AQS

Java併發程式設計核心在於java。util。concurrent包而juc當中的大多數同步器實現都是圍繞著共同的基礎行為,比如等待佇列、條件佇列、獨佔獲取、共享獲取等,而這個行為的抽象就是基於

AbstractQueuedSynchronizer簡稱AQS

,AQS定義了一套多執行緒訪問共享資源的同步器框架,是一個依賴狀態(state)的同步器。

ReentrantLock是一種基於AQS框架的應用實現

,是JDK中的一種執行緒併發訪問的同步手段,它的功能類似於synchronized是一種互斥鎖,可以保證執行緒安全。而且它具有比synchronized更多的特性,比如它支援手動加鎖與解鎖,支援加鎖的公平性。

ReentrantLock的使用:

ReentrantLock lock = new ReentrantLock(false);//false為非公平鎖,true為公平鎖lock。lock() //加鎖// 程式碼邏輯lock。unlock() //解鎖複製程式碼

原始碼分析(基於公平鎖的原始碼)

ReentrantLock的結構繼承關係。

AQS應用之Lock詳解

可以看到引數是false是非公平鎖,true為公平鎖。

AQS應用之Lock詳解

AQS應用之Lock詳解

無論公平還是非公平都繼承了Sync。

AQS應用之Lock詳解

Sync則集成了AbstractQueuedSynchronizer,也就是所要說的AQS。

AQS應用之Lock詳解

發現AbstractQueuedSynchronizer還有一個父類AbstractOwnableSynchronizer。

AQS應用之Lock詳解

在AbstractOwnableSynchronizer中僅只有一個Thread物件,

這個就是當前持有鎖的執行緒

以上就是ReentrantLock的繼承關係。

AbstractQueuedSynchronizer類

AQS應用之Lock詳解

在AbstractQueuedSynchronizer類中,發現擁有

Node、head、tail以及state

四個屬性。

Node:是一種基於雙向連結串列資料結構的佇列,是FIFO先入先出執行緒等待佇列。公平鎖就是基於這個佇列實現的。

head:頭節點。

tail:尾節點。

state:同步狀態。可重入是基於這個狀態實現,每重入一次,state加1,反之減1,直到為0。

接下來看看Node的結構。

AQS應用之Lock詳解

Node是一個搞雙向連結串列的佇列,因此包含有prev和next,分別指向當前節點的前節點和下一個節點,以及thread當前節點的執行緒的引用。waitStatus表示當前節點的生命狀態,有四個值分別是:CANCELLED = 1、SIGNAL = -1、CONDITION = -2、PROPAGATE = -3以及初始狀態0(SIGNAL:可被喚醒,CANCELLED:出現異常,取消,CONDITION:條件等待,PROPAGATE:傳播)。EXCLUSIVE和SHARED分別表示當前節點的模式分為獨佔和共享。

lock方法

AQS應用之Lock詳解

lock方法進去可以發現,它是Sync的抽象方法,在公平鎖和非公平鎖中對其進行了實現。

AQS應用之Lock詳解

在lock中呼叫了acquire方法。

AQS應用之Lock詳解

先來看tryAcquire方法會返回true:加鎖成功和false:加鎖失敗。如果加鎖失敗則會新增到等待佇列中(addWaiter(Node。EXCLUSIVE))。

AQS應用之Lock詳解

在公平鎖和非公平鎖中進行了實現。

AQS應用之Lock詳解

首先,

獲取當前執行緒和獲取state同步狀態

(在AbstractQueuedSynchronizer類中),

如果為0

,說明當前沒有執行緒持有鎖。接著,透過

hasQueuedPredecessors()

判斷是否有其他執行緒在排隊,如果沒有執行緒在排隊,則進行

CAS(即compareAndSetState(0, acquires)方法)操作

,嘗試將當前的

state同步狀態改為1

,修改成功,設定

ExclusiveOwnerThread獨佔執行緒為當前執行緒

,即加鎖成功。如下圖。

AQS應用之Lock詳解

如果獲取state同步狀態,

不為0

,則判斷當前執行緒,是否是獨佔執行緒,即

是否當前執行緒持有了鎖

,如果是則對

state的值加1

,即加鎖成功。當前狀態如下圖。

AQS應用之Lock詳解

如果加鎖失敗,則會呼叫addWaiter(Node。EXCLUSIVE)新增到等待佇列中。

AQS應用之Lock詳解

首先,會使用

當前執行緒和獨佔模式

,來構建一個獨佔的node節點。接著會將tail賦值給pred,因為是第一次進入,tail和head兩個節點都為null,說明當前的等待佇列是空的,因此不會走if判斷,會進入

enq(node)方法

。如果等待佇列不為空,則直接進入if判斷在最後面加入節點。

AQS應用之Lock詳解

這個方法可以看到,可能同一時間會有多個執行緒,執行入隊,因此透過CAS操作,自旋的插入到佇列中直到成功。如果佇列為空的話,則需要進行初始化(Must initialize),會建立一個空的節點,這個節點沒有包含任何的執行緒引用,head和tail同時指向這個空節點。如下圖所示。

AQS應用之Lock詳解

初始化等待佇列完成後,才是真正的開始入隊操作。入隊也存在競爭,也使用CAS操作。如下圖所示。

AQS應用之Lock詳解

進入等待佇列後,執行緒需要被阻塞,阻塞邏輯在acquireQueued(addWaiter(Node。EXCLUSIVE), arg))中實現。

AQS應用之Lock詳解

在這個方法中會發現,在進行阻塞前,會拿出head頭節點

再進行一次tryAcquire(arg)方法,進行搶鎖操作

,因為,執行緒阻塞需要從使用者態到核心態的切換,儘可能的

減少阻塞所帶來的開銷

1。如果拿到鎖,則會setHead(node)重新設定頭節點,並頭節點的下一個節點置空。

AQS應用之Lock詳解

AQS應用之Lock詳解

2。如果沒有拿到鎖,則進入阻塞邏輯。先進入shouldParkAfterFailedAcquire(p, node)方法。

AQS應用之Lock詳解

可以看到,會去拿

前驅節點的waitStatus狀態

來做判斷,如果是

SIGNAL狀態則表示可以被喚醒

,如果大於0則表示,當前節點存在異常。否則,使用CAS操作嘗試將初始狀態0改為SIGNAL狀態。即透過前驅節點的waitStatus來對判斷當前節點是否可以被喚醒。

AQS應用之Lock詳解

首先第一輪迴圈進入shouldParkAfterFailedAcquire(p, node),修改head的狀態,修改成SIGNAL,表示可以被喚醒。第二輪迴圈進入shouldParkAfterFailedAcquire(p, node),才判斷ws=SIGNAL狀態表示可以被喚醒,返回true。才會進入parkAndCheckInterrupt()方法。

AQS應用之Lock詳解

呼叫LockSupport。park(),阻塞住執行緒。

以上即lock的整個原始碼流程。

阻塞住的執行緒再unlock方法中回去喚醒。

unlock方法

AQS應用之Lock詳解

AQS應用之Lock詳解

進入tryRelease(arg)方法。

AQS應用之Lock詳解

即獲state狀態呼叫一次unlock方法就會減1。直到狀態變為0,setExclusiveOwnerThread(null),將獨佔執行緒置為null,更新state狀態。釋放鎖成功,首先會判斷頭節點的waitStatus不為0,才進入unparkSuccessor(h)喚醒下一個節點被阻塞的執行緒。

AQS應用之Lock詳解

首先會判斷ws的狀態如果小於0,用CAS操作,將ws狀態修改回0。因為,在上面lock的時候執行緒進入parkAndCheckInterrupt()方法會被阻塞在這裡,不會往下去執行。這個時候在unlock的最後有unpark()操作,會喚醒執行緒繼續往下parkAndCheckInterrupt()方法執行。在公平鎖中則一定會是佇列的第一個節點拿到鎖,但是如果是非公平鎖,則不一定是佇列的第一個節點搶到鎖,有可能被其他執行緒搶到鎖,這個時候佇列的第一個節點依然要進行阻塞操作,因此,將ws狀態變回0,在第一次迴圈的時候又改為-1。

小結

:在shouldParkAfterFailedAcquire(p, node)中會將head節點的waiteState改為-1,因為,持有鎖的執行緒在釋放鎖的時候需要判斷head節點的waiteState!=0,如果成立,會再把waiteState改為0。要想喚醒排隊的第一個執行緒T1,T1被喚醒,準備繼續走迴圈,搶鎖(acquireQueued方法中)。搶鎖,可能會失敗(在非公平鎖場景下),此時可能有其他執行緒T3持有了鎖。T1可能再次被阻塞,head的節點狀態再一次經歷2次迴圈將waiteState改為-1。

最後

以上原始碼流程均是公平鎖的邏輯。非公平鎖的原始碼邏輯與公平鎖的類似,差別僅在於有無hasQueuedPredecessors()方法的判斷,在此不做贅述。

作者:沒傘的孩子

連結:https://juejin。cn/post/7013956240984244254

著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。