ReentrantLock 以及 AQS 實現原理

2022-07-17 14:06:10 字數 3745 閱讀 6122

reentrantlock是可重入鎖,什麼是可重入鎖呢?可重入鎖就是當前持有該鎖的執行緒能夠多次獲取該鎖,無需等待。可重入鎖是如何實現的呢?這要從reentrantlock的乙個內部類sync的父類說起,sync的父類是abstractqueuedsynchronizer(後面簡稱aqs)。

aqs是jdk1.5提供的乙個基於fifo等待佇列實現的乙個用於實現同步器的基礎框架,這個基礎框架的重要性可以這麼說,juc包裡面幾乎所有的有關鎖、多執行緒併發以及執行緒同步器等重要元件的實現都是基於aqs這個框架。aqs的核心思想是基於volatile int state這樣的乙個屬性同時配合unsafe工具對其原子性的操作來實現對當前鎖的狀態進行修改。當state的值為0的時候,標識改lock不被任何執行緒所占有。

reentrantloc的架構相對簡單,主要包括乙個sync的內部抽象類以及sync抽象類的兩個實現類。上面已經說過了sync繼承自aqs,把aqs的父類abstractownablesynchronizer(後面簡稱aos)也畫了進來,可以稍微提一下,aos主要提供乙個exclusiveownerthread屬性,用於關聯當前持有該鎖的執行緒。另外、sync的兩個實現類分別是nonfairsync和fairsync,由名字大概可以猜到,乙個是用於實現公平鎖、乙個是用於實現非公平鎖。那麼sync為什麼要被設計成內部類呢?我們可以看看aqs主要提供了哪些protect的方法用於修改state的狀態,我們發現sync被設計成為安全的外部不可訪問的內部類。reentrantlock中所有涉及對aqs的訪問都要經過sync,其實,sync被設計成為內部類主要是為了安全性考慮,這也是作者在aqs的comments上強調的一點。

作為aqs的核心實現的一部分,舉個例子來描述一下這個佇列長什麼樣子,我們假設目前有三個執行緒thread1、thread2、thread3同時去競爭鎖,如果結果是thread1獲取了鎖,thread2和thread3進入了等待佇列,aqs的等待佇列基於乙個雙向鍊錶實現的,head節點不關聯執行緒,後面兩個節點分別關聯thread2和thread3,他們將會按照先後順序被串聯在這個佇列上。這個時候如果後面再有執行緒進來的話將會被當做佇列的tail。

public

final

void acquire(int

arg)

三個執行緒同時進來,他們會首先會通過cas去修改state的狀態,如果修改成功,那麼競爭成功,因此這個時候三個執行緒只有乙個cas成功,其他兩個執行緒失敗,也就是tryacquire返回false。

接下來,addwaiter會把將當前執行緒關聯的exclusive型別的節點入佇列:

private

node addwaiter(node mode)

}enq(node);

return

node;

}

如果隊尾節點不為null,則說明佇列中已經有執行緒在等待了,那麼直接入隊尾。對於我們舉的例子,這邊的邏輯應該是走enq,也就是開始隊尾是null,其實這個時候整個佇列都是null的。

private node enq(final

node node)

else}}

}

如果thread2和thread3同時進入了enq,同時t==null,則進行cas操作對佇列進行初始化,這個時候只有乙個執行緒能夠成功,然後他們繼續進入迴圈,第二次都進入了else**塊,這個時候又要進行cas操作,將自己放在隊尾,因此這個時候又是只有乙個執行緒成功,我們假設是thread2成功,哈哈,thread2開心的返回了,thread3失落的再進行下一次的迴圈,最終入佇列成功,返回自己。

基於上面兩段**,他們是如何實現不進行加鎖,當有多個執行緒,或者說很多很多的執行緒同時執行的時候,怎麼能保證最終他們都能夠乖乖的入佇列而不會出現併發問題的呢?這也是這部分**的經典之處,多執行緒競爭,熱點、單點在佇列尾部,多個執行緒都通過【cas+死迴圈】這個free-lock**搭檔來對佇列進行修改,每次能夠保證只有乙個成功,如果失敗下次重試,如果是n個執行緒,那麼每個執行緒最多loop n次,最終都能夠成功。

上面只是addwaiter的實現部分,那麼節點入佇列之後會繼續發生什麼呢?那就要看看acquirequeued是怎麼實現的了,為保證文章整潔,**我就不貼了,同志們自行查閱,我們還是以上面的例子來看看,thread2和thread3已經被放入佇列了,進入acquirequeued之後:

對於thread2來說,它的prev指向head,因此會首先再嘗試獲取鎖一次,如果失敗,則會將head的waitstatus值為signal,下次迴圈的時候再去嘗試獲取鎖,如果還是失敗,且這個時候prev節點的waitstatus已經是signal,則這個時候執行緒會被通過locksupport掛起。

對於thread3來說,它的prev指向thread2,因此直接看看thread2對應的節點的waitstatus是否為signal,如果不是則將它設定為signal,再給自己一次去看看自己有沒有資格獲取鎖,如果thread2還是擋在前面,且它的waitstatus是signal,則將自己掛起。

如果thread1死死的握住鎖不放,那麼thread2和thread3現在的狀態就是掛起狀態啦,而且head,以及thread的waitstatus都是signal,儘管他們在整個過程中曾經數次去嘗試獲取鎖,但是都失敗了,失敗了不能死迴圈呀,所以就被掛起了。

我們來看看當thread1這個時候終於做完了事情,呼叫了unlock準備釋放鎖,這個時候發生了什麼。

public

final

boolean release(int

arg)

return

false

;}

首先,thread1會修改aqs的state狀態,加入之前是1,則變為0,注意這個時候對於非公平鎖來說是個很好的插入機會,舉個例子,如果鎖是公平鎖,這個時候來了thread4,那麼這個鎖將會被thread4搶去。。。

我們繼續走常規路線來分析,當thread1修改完狀態了,判斷佇列是否為null,以及隊頭的waitstatus是否為0,如果waitstatus為0,說明佇列無等待執行緒,按照我們的例子來說,隊頭的waitstatus為signal=-1,因此這個時候要通知佇列的等待執行緒,可以來拿鎖啦,這也是unparksuccessor做的事情,unparksuccessor主要做三件事情:

將隊頭的waitstatus設定為0.

通過從佇列尾部向佇列頭部移動,找到最後乙個waitstatus<=0的那個節點,也就是離隊頭最近的沒有被cancelled的那個節點,隊頭這個時候指向這個節點。

將這個節點喚醒,其實這個時候thread1已經出佇列了。

還記得執行緒在**掛起的麼,上面說過了,在acquirequeued裡面,我沒有貼**,自己去看哦。這裡我們也大概能理解aqs的這個佇列為什麼叫fifo佇列了,因此每次喚醒僅僅喚醒隊頭等待執行緒,讓隊頭等待執行緒先出。

這裡說一下羊群效應,當有多個執行緒去競爭同乙個鎖的時候,假設鎖被某個執行緒占用,那麼如果有成千上萬個執行緒在等待鎖,有一種做法是同時喚醒這成千上萬個執行緒去去競爭鎖,這個時候就發生了羊群效應,海量的競爭必然造成資源的劇增和浪費,因此終究只能有乙個執行緒競爭成功,其他執行緒還是要老老實實的回去等待。aqs的fifo的等待佇列給解決在鎖競爭方面的羊群效應問題提供了乙個思路:保持乙個fifo佇列,佇列每個節點只關心其前乙個節點的狀態,執行緒喚醒也只喚醒隊頭等待執行緒。其實這個思路已經被應用到了分布式鎖的實踐中,見:zookeeper分布式鎖的改進實現方案。

這篇文章粗略的介紹一下reentrantlock以及鎖實現基礎框架aqs的實現原理,大致上通過舉了個三個執行緒競爭鎖的例子,從lock、unlock過程發生了什麼這個問題,深入了解aqs基於狀態的標識以及fifo等待佇列方面的工作原理,最後擴充套件介紹了一下羊群效應問題,博主才疏學淺,還請多多指教。

ReentrantLock實現同步

reentrantlock 也可以實現synchronized方法 塊的同步效果。reentrantlock 實現同步 如下 1 新建乙個service類 public class myservice public static void methodb 2 新建乙個測試類 public class...

ReentrantLock之unlock方法分析

public void unlock public final boolean release int arg return false release 1 嘗試在當前鎖的鎖定計數 state 值上減1。成功返回true,否則返回false。當然在release 方法中不僅僅只是將state 1這麼...

ReentrantLock 原理分析

public void lock 這是lock的原始碼,呼叫的其實是sync這個物件的lock函式,而sync是reentrantlock內部類sync的乙個物件例項,他有兩種實現nonfairsync 非公平鎖 和fairsync 公平鎖 先看公平鎖的lock函式 final void lock ...