阿里P7架構師帶你深入分析AQS實現原理

阿里P7架構師帶你深入分析AQS實現原理

內容導航

  • ReentrantLock重入鎖的使用引入AQS
  • 什么是AQS
  • AQS的源碼分析

簡單解釋一下J.U.C,是JDK中提供的并發工具包, java.util.concurrent。里面提供了很多并發編程中很常用的實用工具類,比如atomic原子操作、比如lock同步鎖、fork/join等。

從Lock作為切入點

我想以lock作為切入點來講解AQS,畢竟同步鎖是解決線程安全問題的通用手段,也是我們工作中用得比較多的方式。

Lock API

Lock是一個接口,方法定義如下

阿里P7架構師帶你深入分析AQS實現原理

Lock的實現

實現Lock接口的類有很多,以下為幾個常見的鎖實現

  • ReentrantLock:表示重入鎖,它是唯一一個實現了Lock接口的類。重入鎖指的是線程在獲得鎖之后,再次獲取該鎖不需要阻塞,而是直接關聯一次計數器增加重入次數
  • ReentrantReadWriteLock:重入讀寫鎖,它實現了ReadWriteLock接口,在這個類中維護了兩個鎖,一個是ReadLock,一個是WriteLock,他們都分別實現了Lock接口。讀寫鎖是一種適合讀多寫少的場景下解決線程安全問題的工具,基本原則是: 讀和讀不互斥、讀和寫互斥、寫和寫互斥。也就是說涉及到影響數據變化的操作都會存在互斥。
  • StampedLock: stampedLock是JDK8引入的新的鎖機制,可以簡單認為是讀寫鎖的一個改進版本,讀寫鎖雖然通過分離讀和寫的功能使得讀和讀之間可以完全并發,但是讀和寫是有沖突的,如果大量的讀線程存在,可能會引起寫線程的饑餓。stampedLock是一種樂觀的讀策略,使得樂觀鎖完全不會阻塞寫線程

ReentrantLock的簡單實用

如何在實際應用中使用ReentrantLock呢?我們通過一個簡單的demo來演示一下

阿里P7架構師帶你深入分析AQS實現原理

這段代碼主要做一件事,就是通過一個靜態的 incr()方法對共享變量 count做連續遞增,在沒有加同步鎖的情況下多線程訪問這個方法一定會存在線程安全問題。所以用到了 ReentrantLock來實現同步鎖,并且在finally語句塊中釋放鎖。那么我來引出一個問題,大家思考一下

多個線程通過lock競爭鎖時,當競爭失敗的鎖是如何實現等待以及被喚醒的呢?

什么是AQS

aqs全稱為AbstractQueuedSynchronizer,它提供了一個FIFO隊列,可以看成是一個用來實現同步鎖以及其他涉及到同步功能的核心組件,常見的有:ReentrantLock、CountDownLatch等。

AQS是一個抽象類,主要是通過繼承的方式來使用,它本身沒有實現任何的同步接口,僅僅是定義了同步狀態的獲取以及釋放的方法來提供自定義的同步組件。

可以這么說,只要搞懂了AQS,那么J.U.C中絕大部分的api都能輕松掌握。

AQS的兩種功能

從使用層面來說,AQS的功能分為兩種:獨占和共享

  • 獨占鎖,每次只能有一個線程持有鎖,比如前面給大家演示的ReentrantLock就是以獨占方式實現的互斥鎖
  • 共享鎖,允許多個線程同時獲取鎖,并發訪問共享資源,比如ReentrantReadWriteLock

ReentrantLock的類圖

仍然以ReentrantLock為例,來分析AQS在重入鎖中的使用。畢竟單純分析AQS沒有太多的含義。先理解這個類圖,可以方便我們理解AQS的原理

阿里P7架構師帶你深入分析AQS實現原理

AQS的內部實現

AQS的實現依賴內部的同步隊列,也就是FIFO的雙向隊列,如果當前線程競爭鎖失敗,那么AQS會把當前線程以及等待狀態信息構造成一個Node加入到同步隊列中,同時再阻塞該線程。當獲取鎖的線程釋放鎖以后,會從隊列中喚醒一個阻塞的節點(線程)。

阿里P7架構師帶你深入分析AQS實現原理

AQS隊列內部維護的是一個FIFO的雙向鏈表,這種結構的特點是每個數據結構都有兩個指針,分別指向直接的后繼節點和直接前驅節點。所以雙向鏈表可以從任意一個節點開始很方便的訪問前驅和后繼。每個Node其實是由線程封裝,當線程爭搶鎖失敗后會封裝成Node加入到ASQ隊列中去

Node類的組成如下

阿里P7架構師帶你深入分析AQS實現原理

釋放鎖以及添加線程對于隊列的變化

添加節點

當出現鎖競爭以及釋放鎖的時候,AQS同步隊列中的節點會發生變化,首先看一下添加節點的場景。

阿里P7架構師帶你深入分析AQS實現原理

這里會涉及到兩個變化

  • 新的線程封裝成Node節點追加到同步隊列中,設置prev節點以及修改當前節點的前置節點的next節點指向自己
  • 通過CAS講tail重新指向新的尾部節點

釋放鎖移除節點

head節點表示獲取鎖成功的節點,當頭結點在釋放同步狀態時,會喚醒后繼節點,如果后繼節點獲得鎖成功,會把自己設置為頭結點,節點的變化過程如下

阿里P7架構師帶你深入分析AQS實現原理

這個過程也是涉及到兩個變化

  • 修改head節點指向下一個獲得鎖的節點
  • 新的獲得鎖的節點,將prev的指針指向null

這里有一個小的變化,就是設置head節點不需要用CAS,原因是設置head節點是由獲得鎖的線程來完成的,而同步鎖只能由一個線程獲得,所以不需要CAS保證,只需要把head節點設置為原首節點的后繼節點,并且斷開原head節點的next引用即可

AQS的源碼分析

清楚了AQS的基本架構以后,我們來分析一下AQS的源碼,仍然以ReentrantLock為模型。

ReentrantLock的時序圖

調用ReentrantLock中的lock()方法,源碼的調用過程我使用了時序圖來展現

阿里P7架構師帶你深入分析AQS實現原理

從圖上可以看出來,當鎖獲取失敗時,會調用addWaiter()方法將當前線程封裝成Node節點加入到AQS隊列,基于這個思路,我們來分析AQS的源碼實現

分析源碼

ReentrantLock.lock()

阿里P7架構師帶你深入分析AQS實現原理

這個是獲取鎖的入口,調用sync這個類里面的方法,sync是什么呢?

阿里P7架構師帶你深入分析AQS實現原理

sync是一個靜態內部類,它繼承了AQS這個抽象類,前面說過AQS是一個同步工具,主要用來實現同步控制。我們在利用這個工具的時候,會繼承它來實現同步控制功能。

通過進一步分析,發現Sync這個類有兩個具體的實現,分別是 NofairSync(非公平鎖), FailSync(公平鎖).

  • 公平鎖 表示所有線程嚴格按照FIFO來獲取鎖
  • 非公平鎖 表示可以存在搶占鎖的功能,也就是說不管當前隊列上是否存在其他線程等待,新線程都有機會搶占鎖

公平鎖和非公平鎖的實現上的差異,我會在文章后面做一個解釋,接下來的分析仍然以 非公平鎖作為主要分析邏輯。

NonfairSync.lock

阿里P7架構師帶你深入分析AQS實現原理

這段代碼簡單解釋一下

  • 由于這里是非公平鎖,所以調用lock方法時,先去通過cas去搶占鎖
  • 如果搶占鎖成功,保存獲得鎖成功的當前線程
  • 搶占鎖失敗,調用acquire來走鎖競爭邏輯

compareAndSetStatecompareAndSetState的代碼實現邏輯如下

阿里P7架構師帶你深入分析AQS實現原理

這段代碼其實邏輯很簡單,就是通過cas樂觀鎖的方式來做比較并替換。上面這段代碼的意思是,如果當前內存中的state的值和預期值expect相等,則替換為update。更新成功返回true,否則返回false.這個操作是原子的,不會出現線程安全問題,這里面涉及到Unsafe這個類的操作,一級涉及到state這個屬性的意義。stateAQS中有一個這樣的屬性定義,這個對于重入鎖的實現來說,表示一個同步狀態。它有兩個含義的表示

如果對java微服務、分布式、高并發、高可用、大型互聯網架構技術、面試經驗交流。
可以加我架構圈子群:[142019080](https://jq.qq.com/?_wv=1027&k=5poqJDD) 領取資料,群內每天更新資料,免費領取。

  • 當state=0時,表示無鎖狀態
  • 當state>0時,表示已經有線程獲得了鎖,也就是state=1,但是因為ReentrantLock允許重入,所以同一個線程多次獲得同步鎖的時候,state會遞增,比如重入5次,那么state=5。 而在釋放鎖的時候,同樣需要釋放5次直到state=0其他線程才有資格獲得鎖

<pre class="ql-align-justify" style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">private volatile int state;
</pre>

需要注意的是:不同的AQS實現,state所表達的含義是不一樣的。UnsafeUnsafe類是在sun.misc包下,不屬于Java標準。但是很多Java的基礎類庫,包括一些被廣泛使用的高性能開發庫都是基于Unsafe類開發的,比如Netty、Hadoop、Kafka等;Unsafe可認為是Java中留下的后門,提供了一些低層次操作,如直接內存訪問、線程調度等

<pre class="ql-align-justify" style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-align: start; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
</pre>

這個是一個native方法, 第一個參數為需要改變的對象,第二個為偏移量(即之前求出來的headOffset的值),第三個參數為期待的值,第四個為更新后的值整個方法的作用是如果當前時刻的值等于預期值var4相等,則更新為新的期望值 var5,如果更新成功,則返回true,否則返回false;

acquire

acquire是AQS中的方法,如果CAS操作未能成功,說明state已經不為0,此時繼續acquire(1)操作,這里大家思考一下,acquire方法中的1的參數是用來做什么呢?如果沒猜中,往前面回顧一下state這個概念

阿里P7架構師帶你深入分析AQS實現原理

這個方法的主要邏輯是

  • 通過tryAcquire嘗試獲取獨占鎖,如果成功返回true,失敗返回false
  • 如果tryAcquire失敗,則會通過addWaiter方法將當前線程封裝成Node添加到AQS隊列尾部
  • acquireQueued,將Node作為參數,通過自旋去嘗試獲取鎖。

NonfairSync.tryAcquire

這個方法的作用是嘗試獲取鎖,如果成功返回true,不成功返回false

它是重寫AQS類中的tryAcquire方法,并且大家仔細看一下AQS中tryAcquire方法的定義,并沒有實現,而是拋出異常。按照一般的思維模式,既然是一個不實現的模版方法,那應該定義成abstract,讓子類來實現呀?大家想想為什么

阿里P7架構師帶你深入分析AQS實現原理

nonfairTryAcquire

tryAcquire(1)在NonfairSync中的實現代碼如下

阿里P7架構師帶你深入分析AQS實現原理
  • 獲取當前線程,判斷當前的鎖的狀態
  • 如果state=0表示當前是無鎖狀態,通過cas更新state狀態的值
  • 如果當前線程是屬于重入,則增加重入次數

addWaiter

當tryAcquire方法獲取鎖失敗以后,則會先調用addWaiter將當前線程封裝成Node,然后添加到AQS隊列

阿里P7架構師帶你深入分析AQS實現原理
  • 將當前線程封裝成Node
  • 判斷當前鏈表中的tail節點是否為空,如果不為空,則通過cas操作把當前線程的node添加到AQS隊列
  • 如果為空或者cas失敗,調用enq將節點添加到AQS隊列

enq

enq就是通過自旋操作把當前節點加入到隊列中

阿里P7架構師帶你深入分析AQS實現原理

假如有兩個線程t1,t2同時進入enq方法,t==null表示隊列是首次使用,需要先初始化

另外一個線程cas失敗,則進入下次循環,通過cas操作將node添加到隊尾

到目前為止,通過addwaiter方法構造了一個AQS隊列,并且將線程添加到了隊列的節點中

acquireQueued

將添加到隊列中的Node作為參數傳入acquireQueued方法,這里面會做搶占鎖的操作

阿里P7架構師帶你深入分析AQS實現原理
  • 獲取當前節點的prev節點
  • 如果prev節點為head節點,那么它就有資格去爭搶鎖,調用tryAcquire搶占鎖
  • 搶占鎖成功以后,把獲得鎖的節點設置為head,并且移除原來的初始化head節點
  • 如果獲得鎖失敗,則根據waitStatus決定是否需要掛起線程
  • 最后,通過cancelAcquire取消獲得鎖的操作

前面的邏輯都很好理解,主要看一下shouldParkAfterFailedAcquire這個方法和parkAndCheckInterrupt的作用

shouldParkAfterFailedAcquire

從上面的分析可以看出,只有隊列的第二個節點可以有機會爭用鎖,如果成功獲取鎖,則此節點晉升為頭節點。對于第三個及以后的節點,if (p == head)條件不成立,首先進行shouldParkAfterFailedAcquire(p, node)操作

shouldParkAfterFailedAcquire方法是判斷一個爭用鎖的線程是否應該被阻塞。它首先判斷一個節點的前置節點的狀態是否為Node.SIGNAL,如果是,是說明此節點已經將狀態設置-如果鎖釋放,則應當通知它,所以它可以安全的阻塞了,返回true。

阿里P7架構師帶你深入分析AQS實現原理

parkAndCheckInterrupt

如果shouldParkAfterFailedAcquire返回了true,則會執行: parkAndCheckInterrupt()方法,它是通過LockSupport.park(this)將當前線程掛起到WATING狀態,它需要等待一個中斷、unpark方法來喚醒它,通過這樣一種FIFO的機制的等待,來實現了Lock的操作。

阿里P7架構師帶你深入分析AQS實現原理

LockSupportLockSupport類是Java6引入的一個類,提供了基本的線程同步原語。LockSupport實際上是調用了Unsafe類里的函數,歸結到Unsafe里,只有兩個函數:

阿里P7架構師帶你深入分析AQS實現原理

unpark函數為線程提供“許可(permit)”,線程調用park函數則等待“許可”。這個有點像信號量,但是這個“許可”是不能疊加的,“許可”是一次性的。permit相當于0/1的開關,默認是0,調用一次unpark就加1變成了1.調用一次park會消費permit,又會變成0。 如果再調用一次park會阻塞,因為permit已經是0了。直到permit變成1.這時調用unpark會把permit設置為1.每個線程都有一個相關的permit,permit最多只有一個,重復調用unpark不會累積

鎖的釋放

ReentrantLock.unlock

加鎖的過程分析完以后,再來分析一下釋放鎖的過程,調用release方法,這個方法里面做兩件事,1,釋放鎖 ;2,喚醒park的線程

阿里P7架構師帶你深入分析AQS實現原理

tryRelease

這個動作可以認為就是一個設置鎖狀態的操作,而且是將狀態減掉傳入的參數值(參數是1),如果結果狀態為0,就將排它鎖的Owner設置為null,以使得其它的線程有機會進行執行。

在排它鎖中,加鎖的時候狀態會增加1(當然可以自己修改這個值),在解鎖的時候減掉1,同一個鎖,在可以重入后,可能會被疊加為2、3、4這些值,只有unlock()的次數與lock()的次數對應才會將Owner線程設置為空,而且也只有這種情況下才會返回true。

阿里P7架構師帶你深入分析AQS實現原理

unparkSuccessor

在方法unparkSuccessor(Node)中,就意味著真正要釋放鎖了,它傳入的是head節點(head節點是占用鎖的節點),當前線程被釋放之后,需要喚醒下一個節點的線程
如果對java微服務、分布式、高并發、高可用、大型互聯網架構技術、面試經驗交流。
可以加我架構圈子群:[142019080](https://jq.qq.com/?_wv=1027&k=5poqJDD) 領取資料,群內每天更新資料,免費領取。

阿里P7架構師帶你深入分析AQS實現原理

總結

通過這篇文章基本將AQS隊列的實現過程做了比較清晰的分析,主要是基于非公平鎖的獨占鎖實現。在獲得同步鎖時,同步器維護一個同步隊列,獲取狀態失敗的線程都會被加入到隊列中并在隊列中進行自旋;移出隊列(或停止自旋)的條件是前驅節點為頭節點且成功獲取了同步狀態。在釋放同步狀態時,同步器調用tryRelease(int arg)方法釋放同步狀態,然后喚醒頭節點的后繼節點。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容