寫在前面
前面對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的結構繼承關係。
可以看到引數是false是非公平鎖,true為公平鎖。
無論公平還是非公平都繼承了Sync。
Sync則集成了AbstractQueuedSynchronizer,也就是所要說的AQS。
發現AbstractQueuedSynchronizer還有一個父類AbstractOwnableSynchronizer。
在AbstractOwnableSynchronizer中僅只有一個Thread物件,
這個就是當前持有鎖的執行緒
。
以上就是ReentrantLock的繼承關係。
AbstractQueuedSynchronizer類
在AbstractQueuedSynchronizer類中,發現擁有
Node、head、tail以及state
四個屬性。
Node:是一種基於雙向連結串列資料結構的佇列,是FIFO先入先出執行緒等待佇列。公平鎖就是基於這個佇列實現的。
head:頭節點。
tail:尾節點。
state:同步狀態。可重入是基於這個狀態實現,每重入一次,state加1,反之減1,直到為0。
接下來看看Node的結構。
Node是一個搞雙向連結串列的佇列,因此包含有prev和next,分別指向當前節點的前節點和下一個節點,以及thread當前節點的執行緒的引用。waitStatus表示當前節點的生命狀態,有四個值分別是:CANCELLED = 1、SIGNAL = -1、CONDITION = -2、PROPAGATE = -3以及初始狀態0(SIGNAL:可被喚醒,CANCELLED:出現異常,取消,CONDITION:條件等待,PROPAGATE:傳播)。EXCLUSIVE和SHARED分別表示當前節點的模式分為獨佔和共享。
lock方法
lock方法進去可以發現,它是Sync的抽象方法,在公平鎖和非公平鎖中對其進行了實現。
在lock中呼叫了acquire方法。
先來看tryAcquire方法會返回true:加鎖成功和false:加鎖失敗。如果加鎖失敗則會新增到等待佇列中(addWaiter(Node。EXCLUSIVE))。
在公平鎖和非公平鎖中進行了實現。
首先,
獲取當前執行緒和獲取state同步狀態
(在AbstractQueuedSynchronizer類中),
如果為0
,說明當前沒有執行緒持有鎖。接著,透過
hasQueuedPredecessors()
判斷是否有其他執行緒在排隊,如果沒有執行緒在排隊,則進行
CAS(即compareAndSetState(0, acquires)方法)操作
,嘗試將當前的
state同步狀態改為1
,修改成功,設定
ExclusiveOwnerThread獨佔執行緒為當前執行緒
,即加鎖成功。如下圖。
如果獲取state同步狀態,
不為0
,則判斷當前執行緒,是否是獨佔執行緒,即
是否當前執行緒持有了鎖
,如果是則對
state的值加1
,即加鎖成功。當前狀態如下圖。
如果加鎖失敗,則會呼叫addWaiter(Node。EXCLUSIVE)新增到等待佇列中。
首先,會使用
當前執行緒和獨佔模式
,來構建一個獨佔的node節點。接著會將tail賦值給pred,因為是第一次進入,tail和head兩個節點都為null,說明當前的等待佇列是空的,因此不會走if判斷,會進入
enq(node)方法
。如果等待佇列不為空,則直接進入if判斷在最後面加入節點。
這個方法可以看到,可能同一時間會有多個執行緒,執行入隊,因此透過CAS操作,自旋的插入到佇列中直到成功。如果佇列為空的話,則需要進行初始化(Must initialize),會建立一個空的節點,這個節點沒有包含任何的執行緒引用,head和tail同時指向這個空節點。如下圖所示。
初始化等待佇列完成後,才是真正的開始入隊操作。入隊也存在競爭,也使用CAS操作。如下圖所示。
進入等待佇列後,執行緒需要被阻塞,阻塞邏輯在acquireQueued(addWaiter(Node。EXCLUSIVE), arg))中實現。
在這個方法中會發現,在進行阻塞前,會拿出head頭節點
再進行一次tryAcquire(arg)方法,進行搶鎖操作
,因為,執行緒阻塞需要從使用者態到核心態的切換,儘可能的
減少阻塞所帶來的開銷
。
1。如果拿到鎖,則會setHead(node)重新設定頭節點,並頭節點的下一個節點置空。
2。如果沒有拿到鎖,則進入阻塞邏輯。先進入shouldParkAfterFailedAcquire(p, node)方法。
可以看到,會去拿
前驅節點的waitStatus狀態
來做判斷,如果是
SIGNAL狀態則表示可以被喚醒
,如果大於0則表示,當前節點存在異常。否則,使用CAS操作嘗試將初始狀態0改為SIGNAL狀態。即透過前驅節點的waitStatus來對判斷當前節點是否可以被喚醒。
首先第一輪迴圈進入shouldParkAfterFailedAcquire(p, node),修改head的狀態,修改成SIGNAL,表示可以被喚醒。第二輪迴圈進入shouldParkAfterFailedAcquire(p, node),才判斷ws=SIGNAL狀態表示可以被喚醒,返回true。才會進入parkAndCheckInterrupt()方法。
呼叫LockSupport。park(),阻塞住執行緒。
以上即lock的整個原始碼流程。
阻塞住的執行緒再unlock方法中回去喚醒。
unlock方法
進入tryRelease(arg)方法。
即獲state狀態呼叫一次unlock方法就會減1。直到狀態變為0,setExclusiveOwnerThread(null),將獨佔執行緒置為null,更新state狀態。釋放鎖成功,首先會判斷頭節點的waitStatus不為0,才進入unparkSuccessor(h)喚醒下一個節點被阻塞的執行緒。
首先會判斷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
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。