AQS & CAS

來源https://kaiwu.lagou.com/course/courseInfo.htm?courseId=67#/detail/pc?id=1864

AQS

AQS 全稱是 Abstract Queued Synchronizer,一般翻譯為同步器扎拣。它是一套實(shí)現(xiàn)多線程同步功能的框架晰洒。
AQS 在源碼中被廣泛使用秽梅,尤其是在 JUC(Java Util Concurrent)中翠肘,比如 ReentrantLock蕾羊、Semaphore、CountDownLatch、ThreadPoolExecutor笤昨。理解 AQS 對(duì)我們理解 JUC 中其他組件至關(guān)重要,并且在實(shí)際開發(fā)中也可以通過自定義 AQS 來實(shí)現(xiàn)各種需求場(chǎng)景握恳。

ReentrantLock

Sync 在 ReentrantLock 有兩種實(shí)現(xiàn):NonfairSync 和 FairSync瞒窒,分別對(duì)應(yīng)非公平鎖和公平鎖。
在非公平鎖中的lock() 方法中乡洼,主要做了如下操作:

  • 如果通過 CAS 設(shè)置變量 State(同步狀態(tài))成功崇裁,表示當(dāng)前線程獲取鎖成功,則將當(dāng)前線程設(shè)置為獨(dú)占線程束昵。
  • 如果通過 CAS 設(shè)置變量 State(同步狀態(tài))失敗拔稳,表示當(dāng)前鎖正在被其他線程持有,則進(jìn)入 Acquire 方法進(jìn)行后續(xù)處理锹雏。

acquire() 方法是一個(gè)比較重要的方法巴比,可以將其拆解為 3 個(gè)主要步驟:

  1. tryAcquire() 方法主要目的是嘗試獲取鎖;
  2. addWaiter() 如果 tryAcquire() 嘗試獲取鎖失敗則調(diào)用 addWaiter 將當(dāng)前線程添加到一個(gè)等待隊(duì)列中礁遵;
  3. acquireQueued 處理加入到隊(duì)列中的節(jié)點(diǎn)轻绞,通過自旋去嘗試獲取鎖,根據(jù)情況將線程掛起或者取消佣耐。

以上 3 個(gè)方法都被定義在 AQS 中政勃,但其中 tryAcquire() 有點(diǎn)特殊,默認(rèn)情況下直接拋異常兼砖,因此它需要在子類中復(fù)寫奸远,也就是說真正的獲取鎖的邏輯由子類同步器自己實(shí)現(xiàn)。

ReentrantLock 中 tryAcquire 的實(shí)現(xiàn)(非公平鎖)如下:


解釋說明:

  • 獲取當(dāng)前線程掖鱼,判斷當(dāng)前的鎖的狀態(tài)然走;
  • 如果 state=0 表示當(dāng)前是無(wú)鎖狀態(tài)援制,通過 cas 更新 state 狀態(tài)的值戏挡,返回 true;
  • 如果當(dāng)前線程屬于重入晨仑,則增加重入次數(shù)褐墅,返回 true;
  • 上述情況都不滿足洪己,則獲取鎖失敗返回 false妥凳。

最后用一張圖表示 ReentrantLock.lock() 過程:


在 ReentrantLock 執(zhí)行 lock() 的過程中,大部分同步機(jī)制的核心邏輯都已經(jīng)在 AQS 中實(shí)現(xiàn)答捕,ReentrantLock 自身只要實(shí)現(xiàn)某些特定步驟下的方法即可逝钥,這種設(shè)計(jì)模式叫作模板模式。如果你做過 Android 開發(fā)對(duì)這一模式應(yīng)該非常熟悉拱镐。Activity 的生命周期的執(zhí)行流程都已經(jīng)在 framework 中定義好了艘款,子類 Activity 只要在相應(yīng)的 onCreate持际、onPause 等生命周期方法中提供相應(yīng)的實(shí)現(xiàn)即可。

注意:不只 ReentrantLock哗咆,JUC 包中其他組件例如 CountDownLatch蜘欲、Semaphor 等都是通過一個(gè)內(nèi)部類 Sync 來繼承 AQS,然后在內(nèi)部中通過操作 Sync 來實(shí)現(xiàn)同步晌柬。這種做法的好處是將線程控制的邏輯控制在 Sync 內(nèi)部姥份,而對(duì)外面向用戶提供的接口是自定義鎖,這種聚合關(guān)系能夠很好的解耦兩者所關(guān)注的邏輯年碘。

AQS 核心功能原理分析

首先看下 AQS 中幾個(gè)關(guān)鍵的屬性澈歉,如下所示:


代碼中展示了 AQS 中兩個(gè)比較重要的屬性 Node 和 state。

state 鎖狀態(tài)

state 表示當(dāng)前鎖狀態(tài)盛泡。當(dāng) state = 0 時(shí)表示無(wú)鎖狀態(tài)闷祥;當(dāng) state>0 時(shí),表示已經(jīng)有線程獲得了鎖傲诵,也就是 state=1凯砍,如果同一個(gè)線程多次獲得同步鎖的時(shí)候,state 會(huì)遞增拴竹,比如重入 5 次悟衩,那么 state=5。 而在釋放鎖的時(shí)候栓拜,同樣需要釋放 5 次直到 state=0座泳,其他線程才有資格獲得鎖。

state 還有一個(gè)功能是實(shí)現(xiàn)鎖的獨(dú)占模式或者共享模式幕与。

  • 獨(dú)占模式:只有一個(gè)線程能夠持有同步鎖挑势。
    比如在獨(dú)占模式下,我們可以把 state 的初始值設(shè)置成 0啦鸣,當(dāng)某個(gè)線程申請(qǐng)鎖對(duì)象時(shí)潮饱,需要判斷 state 的值是不是 0,如果不是 0 的話意味著其他線程已經(jīng)持有該鎖诫给,則本線程需要阻塞等待香拉。

  • 共享模式:可以有多個(gè)線程持有同步鎖。
    在共享模式下的道理也差不多中狂,比如說某項(xiàng)操作我們?cè)试S 10 個(gè)線程同時(shí)進(jìn)行凫碌,超過這個(gè)數(shù)量的線程就需要阻塞等待。那么只需要在線程申請(qǐng)對(duì)象時(shí)判斷 state 的值是否小于 10胃榕。如果小于 10盛险,就將 state 加 1 后繼續(xù)同步語(yǔ)句的執(zhí)行;如果等于 10,說明已經(jīng)有 10 個(gè)線程在同時(shí)執(zhí)行該操作苦掘,本線程需要阻塞等待泉褐。

Node 雙端隊(duì)列節(jié)點(diǎn)
Node 是一個(gè)先進(jìn)先出的雙端隊(duì)列,并且是等待隊(duì)列鸟蜡,當(dāng)多線程爭(zhēng)用資源被阻塞時(shí)會(huì)進(jìn)入此隊(duì)列膜赃。這個(gè)隊(duì)列是 AQS 實(shí)現(xiàn)多線程同步的核心。

從之前 ReentrantLock 圖中可以看到揉忘,在 AQS 中有兩個(gè) Node 的指針跳座,分別指向隊(duì)列的 head 和 tail。

Node 的主要結(jié)構(gòu)如下:

默認(rèn)情況下泣矛,AQS 中的鏈表結(jié)構(gòu)如下圖所示:

獲取鎖失敗后續(xù)流程分析
鎖的意義就是使競(jìng)爭(zhēng)到鎖對(duì)象的線程執(zhí)行同步代碼疲眷,多個(gè)線程競(jìng)爭(zhēng)鎖時(shí),競(jìng)爭(zhēng)失敗的線程需要被阻塞等待后續(xù)喚醒您朽。那么 ReentrantLock 是如何實(shí)現(xiàn)讓線程等待并喚醒的呢狂丝?

前面中我們提到在 ReentrantLock.lock() 階段,在 acquire() 方法中會(huì)先后調(diào)用 tryAcquire哗总、addWaiter几颜、acquireQueued 這 3 個(gè)方法來處理。tryAcquire 在 ReentrantLock 中被復(fù)寫并實(shí)現(xiàn)讯屈,如果返回 true 說明成功獲取鎖蛋哭,就繼續(xù)執(zhí)行同步代碼語(yǔ)句′棠福可是如果 tryAcquire 返回 false谆趾,也就是當(dāng)前鎖對(duì)象被其他線程所持有,那么當(dāng)前線程會(huì)被 AQS 如何處理呢叛本?

addWaiter
首先當(dāng)前獲取鎖失敗的線程會(huì)被添加到一個(gè)等待隊(duì)列的末端沪蓬,具體源碼如下:

有兩種情況會(huì)致使插入隊(duì)列失敗:

tail 為空:說明隊(duì)列從未初始化来候,因此需要調(diào)用 enq 方法在隊(duì)列中插入一個(gè)空的 Node跷叉;
compareAndSetTail 失敗:說明插入過程中有線程修改了此隊(duì)列吠勘,因此需要調(diào)用 enq 將當(dāng)前 node 重新插入到隊(duì)列末端性芬。
經(jīng)過 addWaiter 方法之后峡眶,此時(shí)線程以 Node 的方式被加入到隊(duì)列的末端剧防,但是線程還沒有被執(zhí)行阻塞操作,真正的阻塞操作是在下面的 acquireQueued 方法中判斷執(zhí)行辫樱。

acquireQueued
在 acquireQueued 方法中并不會(huì)立即掛起該節(jié)點(diǎn)中的線程峭拘,因此在插入節(jié)點(diǎn)的過程中,之前持有鎖的線程可能已經(jīng)執(zhí)行完畢并釋放鎖,所以這里使用自旋再次去嘗試獲取鎖(不放過任何優(yōu)化細(xì)節(jié))鸡挠。如果自旋操作還是沒有獲取到鎖辉饱!那么就將該線程掛起(阻塞),該方法的源碼如下:


可以看出在 shouldParkAfterFailedAcquire 方法中會(huì)判讀當(dāng)前線程是否應(yīng)該被掛起拣展,其代碼如下:


首先獲取前驅(qū)節(jié)點(diǎn)的 waitStatus 值彭沼,Node 中的 waitStatus 一共有 5 種取值,分別代表的意義如下:


接下來根據(jù) waitStatus 不同的值進(jìn)行不同的操作备埃,主要有以下幾種情況:

  • 如果 waitStatus 等于 SIGNAL姓惑,返回 true 將當(dāng)前線程掛起,等待后續(xù)喚醒操作即可按脚。
  • 如果 waitStatus 大于 0 也就是 CANCLE 狀態(tài)于毙,會(huì)將此前驅(qū)節(jié)點(diǎn)從隊(duì)列中刪除,并在循環(huán)中逐步尋找下一個(gè)不是“CANCEL”狀態(tài)的節(jié)點(diǎn)作為當(dāng)前節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)辅搬。
  • 如果 waitStatus 既不是 SIGNAL 也不是 CANCEL唯沮,則將當(dāng)前節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)狀態(tài)設(shè)置為 SIGNAL,這樣做的好處是下一次執(zhí)行 shouldParkAfterFailedAcquire 時(shí)可以直接返回 true,掛起線程帕识。

代碼再回到 acquireQueued 中沃测,如果 shouldParkAfterFailedAcquire 返回 true 表示線程需要被掛起,那么會(huì)繼續(xù)調(diào)用 parkAndCheckInterrupt 方法執(zhí)行真正的阻塞線程代碼甘耿,具體如下:


這個(gè)方法比較簡(jiǎn)單,只是調(diào)用了 LockSupport 中的 park 方法竿滨。在 LockSupport.park() 方法中調(diào)用了 Unsafe API 來執(zhí)行底層 native 方法將線程掛起佳恬,代碼到這已經(jīng)到了操作系統(tǒng)的層面,沒有必要再深入分析于游。

至此毁葱,獲取鎖的大體流程已經(jīng)分析完畢,總結(jié)一下整個(gè)過程如下:

  • AQS 的模板方法 acquire 通過調(diào)用子類自定義實(shí)現(xiàn)的 tryAcquire 獲取鎖贰剥;
  • 如果獲取鎖失敗倾剿,通過 addWaiter 方法將線程構(gòu)造成 Node 節(jié)點(diǎn)插入到同步隊(duì)列隊(duì)尾;
  • 在 acquirQueued 方法中以自旋的方法嘗試獲取鎖蚌成,如果失敗則判斷是否需要將當(dāng)前線程阻塞前痘,如果需要阻塞則最終執(zhí)行 LockSupport(Unsafe) 中的 native API 來實(shí)現(xiàn)線程阻塞。

釋放鎖流程分析

在上面加鎖階段被阻塞的線程需要被喚醒過后才可以重新執(zhí)行担忧。那具體 AQS 是何時(shí)嘗試喚醒等待隊(duì)列中被阻塞的線程呢芹缔?

同加鎖過程一樣,釋放鎖需要從 ReentrantLock.unlock() 方法開始:


可以看出瓶盛,首先調(diào)用 tryRelease 方法嘗試釋放鎖最欠,如果成功最終會(huì)調(diào)用 AQS 中的 unparkSuccessor 方法來實(shí)現(xiàn)釋放鎖的操作示罗。unparkSuccessor 的具體實(shí)現(xiàn)如下:



解釋說明:

首先獲取當(dāng)前節(jié)點(diǎn)(實(shí)際上傳入的是 head 節(jié)點(diǎn))的狀態(tài),如果 head 節(jié)點(diǎn)的下一個(gè)節(jié)點(diǎn)是 null芝硬,或者下一個(gè)節(jié)點(diǎn)的狀態(tài)為 CANCEL蚜点,則從等待隊(duì)列的尾部開始遍歷,直到尋找第一個(gè) waitStatus 小于 0 的節(jié)點(diǎn)拌阴。

如果最終遍歷到的節(jié)點(diǎn)不為 null绍绘,再調(diào)用 LockSupport.unpark 方法,調(diào)用底層方法喚醒線程迟赃。 至此脯倒,線程被喚醒的時(shí)機(jī)也分析完畢。


不得不說的 CAS

不管是在加鎖還是釋放鎖階段捺氢,多次提到了一種通用的操作:compareAndSetXXX藻丢。這種操作最終會(huì)調(diào)用 Unsafe 中的 API 進(jìn)行 CAS 操作。

CAS 全稱是 Compare And Swap摄乒,譯為比較和替換悠反,是一種通過硬件實(shí)現(xiàn)并發(fā)安全的常用技術(shù),底層通過利用 CPU 的 CAS 指令對(duì)緩存加鎖或總線加鎖的方式來實(shí)現(xiàn)多處理器之間的原子操作馍佑。

它的實(shí)現(xiàn)過程主要有 3 個(gè)操作數(shù):內(nèi)存值 V斋否,舊的預(yù)期值 E,要修改的新值 U拭荤,當(dāng)且僅當(dāng)預(yù)期值 E和內(nèi)存值 V 相同時(shí)茵臭,才將內(nèi)存值 V 修改為 U,否則什么都不做舅世。

CAS 底層會(huì)根據(jù)操作系統(tǒng)和處理器的不同來選擇對(duì)應(yīng)的調(diào)用代碼旦委,以 Windows 和 X86 處理器為例,如果是多處理器雏亚,通過帶 lock 前綴的 cmpxchg 指令對(duì)緩存加鎖或總線加鎖的方式來實(shí)現(xiàn)多處理器之間的原子操作缨硝;如果是單處理器,通過 cmpxchg 指令完成原子操作罢低。

自定義 AQS

理解了 AQS 的設(shè)計(jì)思路查辩,接下來我們就可以通過自定義 AQS 來實(shí)現(xiàn)自己的同步實(shí)現(xiàn)機(jī)制。


代碼中的 MyLock 就是一個(gè)最簡(jiǎn)單的獨(dú)占鎖网持,通過使用 MyLock 也能實(shí)現(xiàn)同 synchronized 和 ReentrantLock 相同的功能宜岛。比如如下代碼:


最終打印的 count 值為 20000,說明兩個(gè)線程之間是線程安全的同步操作功舀。

總結(jié)

總體來說萍倡,AQS 是一套框架,在框架內(nèi)部已經(jīng)封裝好了大部分同步需要的邏輯日杈,在 AQS 內(nèi)部維護(hù)了一個(gè)狀態(tài)指示器 state 和一個(gè)等待隊(duì)列 Node遣铝,而通過 state 的操作又分為兩種:獨(dú)占式和共享式,這就導(dǎo)致 AQS 有兩種不同的實(shí)現(xiàn):獨(dú)占鎖(ReentrantLock 等)和分享鎖(CountDownLatch莉擒、讀寫鎖等)酿炸。本課時(shí)主要從獨(dú)占鎖的角度深入分析了 AQS 的加鎖和釋放鎖的流程。

理解 AQS 的原理對(duì)理解 JUC 包中其他組件實(shí)現(xiàn)的基礎(chǔ)有幫助涨冀,并且理解其原理才能更好的擴(kuò)展其功能填硕。上層開發(fā)人員可以基于此框架基礎(chǔ)上進(jìn)行擴(kuò)展實(shí)現(xiàn)適合不同場(chǎng)景、不同功能的鎖鹿鳖。其中幾個(gè)有可能需要子類同步器實(shí)現(xiàn)的方法如下扁眯。

  • lock()。
  • tryAcquire(int):獨(dú)占方式翅帜。嘗試獲取資源姻檀,成功則返回 true,失敗則返回 false涝滴。
  • tryRelease(int):獨(dú)占方式绣版。嘗試釋放資源,成功則返回 true歼疮,失敗則返回 false杂抽。
  • tryAcquireShared(int):共享方式。嘗試獲取資源韩脏。負(fù)數(shù)表示失斔豸铩;0 表示成功赡矢,但沒有剩余可用資源杭朱;正數(shù)表示成功,且有剩余資源吹散。
  • tryReleaseShared(int):共享方式痕檬。嘗試釋放資源,如果釋放后允許喚醒后續(xù)等待結(jié)點(diǎn)返回 true送浊,否則返回 false梦谜。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市袭景,隨后出現(xiàn)的幾起案子唁桩,更是在濱河造成了極大的恐慌,老刑警劉巖耸棒,帶你破解...
    沈念sama閱讀 222,104評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件荒澡,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡与殃,警方通過查閱死者的電腦和手機(jī)单山,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門碍现,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人米奸,你說我怎么就攤上這事昼接。” “怎么了悴晰?”我有些...
    開封第一講書人閱讀 168,697評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵慢睡,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我铡溪,道長(zhǎng)漂辐,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,836評(píng)論 1 298
  • 正文 為了忘掉前任棕硫,我火速辦了婚禮髓涯,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘哈扮。我一直安慰自己复凳,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評(píng)論 6 397
  • 文/花漫 我一把揭開白布灶泵。 她就那樣靜靜地躺著育八,像睡著了一般。 火紅的嫁衣襯著肌膚如雪赦邻。 梳的紋絲不亂的頭發(fā)上髓棋,一...
    開封第一講書人閱讀 52,441評(píng)論 1 310
  • 那天,我揣著相機(jī)與錄音惶洲,去河邊找鬼按声。 笑死,一個(gè)胖子當(dāng)著我的面吹牛恬吕,可吹牛的內(nèi)容都是我干的签则。 我是一名探鬼主播,決...
    沈念sama閱讀 40,992評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼铐料,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼渐裂!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起钠惩,我...
    開封第一講書人閱讀 39,899評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤柒凉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后篓跛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體膝捞,經(jīng)...
    沈念sama閱讀 46,457評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評(píng)論 3 341
  • 正文 我和宋清朗相戀三年愧沟,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蔬咬。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鲤遥。...
    茶點(diǎn)故事閱讀 40,664評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖林艘,靈堂內(nèi)的尸體忽然破棺而出盖奈,到底是詐尸還是另有隱情,我是刑警寧澤北启,帶...
    沈念sama閱讀 36,346評(píng)論 5 350
  • 正文 年R本政府宣布卜朗,位于F島的核電站拔第,受9級(jí)特大地震影響咕村,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蚊俺,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評(píng)論 3 334
  • 文/蒙蒙 一懈涛、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧泳猬,春花似錦批钠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至忙上,卻和暖如春拷呆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背疫粥。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評(píng)論 1 272
  • 我被黑心中介騙來泰國(guó)打工茬斧, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人梗逮。 一個(gè)月前我還...
    沈念sama閱讀 49,081評(píng)論 3 377
  • 正文 我出身青樓项秉,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親慷彤。 傳聞我的和親對(duì)象是個(gè)殘疾皇子娄蔼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評(píng)論 2 359