一文帶你看懂Java中的Lock鎖底層AQS到底是如何實現的

三友的java日記發表於2022-05-27

前言

相信大家對Java中的Lock鎖應該不會陌生,比如ReentrantLock,鎖主要是用來解決解決多執行緒執行訪問共享資源時的執行緒安全問題。那你是不是很好奇,這些Lock鎖api是如何實現的呢?本文就是來探討一下這些Lock鎖底層的AQS(AbstractQueuedSynchronizer)到底是如何實現的。

本文是基於ReentrantLock來講解,ReentrantLock加鎖只是對AQS的api的呼叫,底層的鎖的狀態(state)和其他執行緒等待(Node雙向連結串列)的過程其實是由AQS來維護的

加鎖

我們先來看看加鎖的過程,先看原始碼,然後模擬兩個執行緒來加鎖的過程。

一文帶你看懂Java中的Lock鎖底層AQS到底是如何實現的

上圖是ReentrantLock的部分實現。裡面有一個Sync的內部類的例項變數,這個Sync內部類繼承自AQS,Sync子類就包括公平鎖和非公平鎖的實現。說白了其實ReentrantLock是通過Sync的子類來實現加鎖。

我們就來看一下Sync的非公平鎖的實現NonfairSync。

一文帶你看懂Java中的Lock鎖底層AQS到底是如何實現的

重寫了它的lock加鎖方法,在實現中因為是非公平的,所以一進來會先通過cas嘗試將AQS類的state引數改為1,直接嘗試加鎖。如果嘗試加鎖失敗會呼叫AQS的acquire方法繼續嘗試加鎖。

假設這裡有個執行緒1先來呼叫lock方法,那麼此時沒有人加鎖,那麼就通過CAS操作,將AQS中的state中的變數由0改為1,代表有人來加鎖,然後將加鎖的執行緒設定為自己如圖。

一文帶你看懂Java中的Lock鎖底層AQS到底是如何實現的

那麼此時有另一個執行緒2來加鎖,發現通過CAS操作會失敗,因為state已經被設定為1了,執行緒執行緒2就會設定失敗,那麼此時就會走else,呼叫AQS的acquire方法繼續嘗試加鎖。

一文帶你看懂Java中的Lock鎖底層AQS到底是如何實現的

進入到acquire會先呼叫tryAcquire再次嘗試加鎖,而這個tryAcquire方法AQS其實是沒有什麼實現的,會呼叫到NonfairSync裡面的tryAcquire,而tryAcquire實際會呼叫到Sync內部類裡面的nonfairTryAcquire非公平嘗試加鎖方法。

一文帶你看懂Java中的Lock鎖底層AQS到底是如何實現的
 
一文帶你看懂Java中的Lock鎖底層AQS到底是如何實現的

先獲取鎖的狀態,判斷鎖的狀態是不是等於0,等於0說明沒人加鎖,可以嘗試去加,如果被加鎖了,就會走else if,else if會判斷加鎖的執行緒是不是當前執行緒,是的話就給state 加 1,代表當前執行緒加了2次鎖,就是可重入鎖的意思(所謂的可重入就是代表一個執行緒可以多次獲取到鎖,只是將state 設定為多次,當執行緒多次釋放鎖之後,將state 設定為0才代表當前執行緒完全釋放了鎖)。

這裡所有的條件假設都不成立。也就是執行緒2嘗試加鎖的時候,執行緒1並沒有釋放鎖,那麼這個方法就會返回false。

接下來就會走到addWaiter方法,這個方法很重要,就是將當前執行緒封裝成一個Node,然後將這個Node放入雙向連結串列中。addWaiter先根據指定模式建立指定的node節點,因為ReentrantLock是獨佔模式,所以傳進去的EXCLUSIVE,這裡通過當前執行緒和模式傳入,初始化一個雙向node節點,獲取最後一個節點,根據最後一個節點是否存在來操作當前節點的父級。如果尾節點不存在會去呼叫enq去初始化

一文帶你看懂Java中的Lock鎖底層AQS到底是如何實現的

放入連結串列中之後如圖。

一文帶你看懂Java中的Lock鎖底層AQS到底是如何實現的

然後呼叫acquireQueued方法

一文帶你看懂Java中的Lock鎖底層AQS到底是如何實現的

這個方法一進來也會嘗試將當前節點去加鎖,然後如果加鎖成功就將當前節點設定為頭節點,最後將當前執行緒中斷,等待喚醒。

執行緒2進來的時候,剛好執行緒2的前一個節點是頭節點,但是不巧的是呼叫tryAcquire方法,還是失敗,那麼此時就會走shouldParkAfterFailedAcquire方法,這個方法是線上程休眠之前呼叫的,很重要,我們來看看幹了什麼事。

一文帶你看懂Java中的Lock鎖底層AQS到底是如何實現的

判斷當前節點的父級節點的狀態,如果父級狀態是-1,則代表當前執行緒可以被喚醒了。如果父級的狀態為取消狀態(什麼叫非取消狀態,就是tryLock方法等待了一些時間沒獲取到鎖的執行緒就處於取消狀態)就跳過父級,尋找下一個可以被喚醒的父級,然後繫結上節點關係,最後將父級的狀態更改為-1。也就說,執行緒(Node)加入佇列之後,如果沒有獲取到鎖,在睡眠之前,會將當前節點的前一個節點設定為非取消狀態的節點,然後將前一個節點的waitStatus設定為-1,代表前一個節點在釋放鎖的時候需要喚醒下一個節點。這一步驟主要是防止當前休眠的執行緒無法被喚醒。這一切設定成功之後,就會返回true。

接下來就會呼叫parkAndCheckInterrupt

一文帶你看懂Java中的Lock鎖底層AQS到底是如何實現的

,這個方法內部呼叫LockSupport.park方法,此時當前執行緒就會休眠。

到這一步執行緒2由於沒有獲取到鎖,就會在這裡休眠等待被喚醒。

來總結一下加鎖的過程。

執行緒1先過來,發現沒人加鎖,那麼此時就會加上鎖。此時執行緒2過來,線上程2加鎖的過程中,執行緒1始終沒有釋放鎖,那麼執行緒2就不會加鎖成功(如果線上程2加鎖的過程中執行緒1始終釋放鎖,那麼執行緒2就會加鎖成功),執行緒2沒有加鎖成功,就會將自己當前執行緒加入等待佇列中(如果沒有佇列就先初始化一個),然後設定前一個節點的狀態,最後通過LockSupport.park方法,將自己這個執行緒休眠。

如果後面還有執行緒3,執行緒4等等諸多的先過來,那麼這些執行緒都會按照前面執行緒2的步驟,將自己插入連結串列後面再休眠。

釋放鎖

ok,說完加鎖的過程之後,我們來看看釋放鎖幹了什麼。

ReentrantLock的unlock其實是呼叫AQS的release方法,我們直接進入release方法,看看是如何實現的

一文帶你看懂Java中的Lock鎖底層AQS到底是如何實現的

進入tryRelease方法,看一下Sync的實現

一文帶你看懂Java中的Lock鎖底層AQS到底是如何實現的

其實很簡單,就是判斷鎖的狀態,也就是加了幾次鎖,然後減去釋放的,最後判斷釋放之後,鎖的狀態是不是0(因為可能執行緒加了多次鎖,所以得判斷一下),是的話說明當前這個鎖已經釋放完了,然後將佔有鎖的執行緒設定為null,然後返回true,

然後就會走接下來的程式碼。

就是判斷當前連結串列頭節點是不是需要喚醒佇列中的執行緒。如果有連結串列的話,頭結點的waitStatus肯定不是0,因為執行緒休眠之前,會將前一個節點的狀態設定為-1,上面加鎖的過程中有提到過。

接下來就會走unparkSuccessor方法,successor代表繼承者的意思,見名知意,這個方法其實就會喚醒當前執行緒中離頭節點最近的沒有狀態為非取消的執行緒。然後呼叫LockSupport.unpark,喚醒等待的線

一文帶你看懂Java中的Lock鎖底層AQS到底是如何實現的

然後執行緒就會從阻塞的那裡甦醒過來,繼續嘗試獲取鎖。

我再次貼出這段程式碼。

一文帶你看懂Java中的Lock鎖底層AQS到底是如何實現的

獲取到鎖之後,就將頭節點設定成自己。

對應我們的例子,就是執行緒1釋放鎖之後,就會喚醒在佇列中執行緒2,先成2獲取到鎖之後,就會將自己前一個節點(也就是頭節點)從連結串列中移除,將自己設定成頭節點。該方法就會跳出死迴圈。

一文帶你看懂Java中的Lock鎖底層AQS到底是如何實現的

到這裡,釋放鎖的過程就講完了,其實很簡單,就是當執行緒完完全全釋放了鎖,會喚醒當前連結串列中的沒有取消的,離頭結點最近的節點(一般就是連結串列中的第二個節點),然後被喚醒的節點就會獲取到鎖,將頭節點設定為自己。

總結

相信看完這篇文章,大家對AQS的底層有了更深層次的瞭解。AQS其實就是內部維護一個鎖的狀態變數state和一個雙向連結串列,加鎖成功就將state的值加1,加鎖失敗就將自己當前執行緒放入連結串列的尾部,然後休眠,等待其他執行緒完完全全釋放鎖之後將自己喚醒,喚醒之後會嘗試加鎖,加鎖成功就會執行業務程式碼了。

到這裡本文就結束了,如果你有什麼疑問歡迎私信告訴我。

如果覺得這篇文章對你有所幫助,還請幫忙點贊、關注、轉發一下,碼字不易,非常感謝!

如果你想聯絡我,歡迎關注我的個人微信公眾號 三友的java日記,每天都會發布技術性的文章,期待與你一起進步。

相關文章