AQS 全稱(chēng)是 Abstract Queued Synchronizer,一般翻譯為同步器。它是一套實(shí)現(xiàn)多線(xiàn)程同步功能的框架路狮。AQS 在源碼中被廣泛使用洞斯,尤其是在 JUC(Java Util Concurrent)中毡庆,比如 ReentrantLock(跟Java的?synchronized 功能差不多,但是拓展性比?synchronized?好)烙如、Semaphore(信號(hào)量么抗,更好控制并發(fā)數(shù))、CountDownLatch(更好實(shí)現(xiàn)線(xiàn)程間交互亚铁。例如可以實(shí)現(xiàn)九宮格圖片集體加載完蝇刀,再進(jìn)行下一步。已經(jīng)加載完的進(jìn)入阻塞刀闷,等待其他線(xiàn)程加載完)熊泵、ThreadPoolExecutor(線(xiàn)程池)。
ReentrantLock 和 AQS 的關(guān)系
本課主要通過(guò) ReentrantLock 來(lái)理解 AQS 內(nèi)部的工作機(jī)制甸昏。首先從 ReentrantLock 的 lock() 方法開(kāi)始:
代碼很簡(jiǎn)單顽分,只是調(diào)用了一個(gè) Sync 的 lock() 方法。
可以看出施蜜,Sync 是 ReentrantLock 中的一個(gè)內(nèi)部類(lèi)卒蘸。ReentrantLock 并沒(méi)有直接繼承 AQS,而是通過(guò)內(nèi)部類(lèi) Sync 來(lái)擴(kuò)展 AQS 的功能翻默,然后 ReentrantLock 中存有 Sync 的全局變量引用缸沃。
Sync 在 ReentrantLock 有兩種實(shí)現(xiàn):NonfairSync 和 FairSync,分別對(duì)應(yīng)非公平鎖和公平鎖修械。以非公平鎖為例趾牧,實(shí)現(xiàn)源碼如下:
可以看出在非公平鎖中的 lock() 方法中,主要做了如下操作:
?1.如果通過(guò) CAS 設(shè)置變量 State(同步狀態(tài))成功肯污,表示當(dāng)前線(xiàn)程獲取鎖成功翘单,則將當(dāng)前線(xiàn)程設(shè)置為獨(dú)占線(xiàn)程吨枉。
?2.如果通過(guò) CAS 設(shè)置變量 State(同步狀態(tài))失敗,表示當(dāng)前鎖正在被其他線(xiàn)程持有哄芜,則進(jìn)入 Acquire 方法進(jìn)行后續(xù)處理貌亭。
acruire() 方法定義在 AQS 中,具體如下:
acquire() 方法是一個(gè)比較重要的方法认臊,可以將其拆解為 3 個(gè)主要步驟:
?1.tryAcquire() 方法主要目的是嘗試獲取鎖圃庭;
?2.addWaiter() 如果 tryAcquire() 嘗試獲取鎖失敗則調(diào)用 addWaiter 將當(dāng)前線(xiàn)程添加到一個(gè)等待隊(duì)列中;
?3.acquireQueued 處理加入到隊(duì)列中的節(jié)點(diǎn)失晴,通過(guò)自旋去嘗試獲取鎖剧腻,根據(jù)情況將線(xiàn)程掛起或者取消。
以上 3 個(gè)方法都被定義在 AQS 中师坎,但其中 tryAcquire() 有點(diǎn)特殊恕酸,其實(shí)現(xiàn)如下:
默認(rèn)情況下直接拋異常,因此它需要在子類(lèi)中復(fù)寫(xiě)胯陋,也就是說(shuō)真正的獲取鎖的邏輯由子類(lèi)同步器自己實(shí)現(xiàn)蕊温。
ReentrantLock 中 tryAcquire 的實(shí)現(xiàn)(非公平鎖)如下:
解釋說(shuō)明:
?1.獲取當(dāng)前線(xiàn)程,判斷當(dāng)前的鎖的狀態(tài)遏乔;
?2.如果 state=0 表示當(dāng)前是無(wú)鎖狀態(tài)义矛,通過(guò) cas 更新 state 狀態(tài)的值,返回 true盟萨;
?3. 如果當(dāng)前線(xiàn)程屬于重入凉翻,則增加重入次數(shù),返回 true捻激;
?4.上述情況都不滿(mǎn)足制轰,則獲取鎖失敗返回 false。
最后用一張圖表示 ReentrantLock.lock() 過(guò)程:
從圖中我們可以看出胞谭,在 ReentrantLock 執(zhí)行 lock() 的過(guò)程中垃杖,大部分同步機(jī)制的核心邏輯都已經(jīng)在 AQS 中實(shí)現(xiàn),ReentrantLock 自身只要實(shí)現(xiàn)某些特定步驟下的方法即可丈屹,這種設(shè)計(jì)模式叫作模板模式调俘。Activity 的生命周期方法就是使用模版模式, Activity 的生命周期的執(zhí)行流程都已經(jīng)在 framework 中定義好了,子類(lèi) Activity 只要在相應(yīng)的 onCreate旺垒、onPause 等生命周期方法中提供相應(yīng)的實(shí)現(xiàn)即可彩库。
注意:不只 ReentrantLock,JUC 包中其他組件例如 CountDownLatch先蒋、Semaphor 等都是通過(guò)一個(gè)內(nèi)部類(lèi) Sync 來(lái)繼承 AQS骇钦,然后在內(nèi)部中通過(guò)操作 Sync 來(lái)實(shí)現(xiàn)同步凛澎。這種做法的好處是將線(xiàn)程控制的邏輯控制在 Sync 內(nèi)部慷彤,而對(duì)外面向用戶(hù)提供的接口是自定義鎖疲酌,這種聚合關(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)有線(xiàn)程獲得了鎖,也就是 state=1糊闽,如果同一個(gè)線(xiàn)程多次獲得同步鎖的時(shí)候梳玫,state 會(huì)遞增,比如重入 5 次右犹,那么 state=5提澎。 而在釋放鎖的時(shí)候,同樣需要釋放 5 次直到 state=0念链,其他線(xiàn)程才有資格獲得鎖盼忌。
state 還有一個(gè)功能是實(shí)現(xiàn)鎖的獨(dú)占模式或者共享模式。
獨(dú)占模式:只有一個(gè)線(xiàn)程能夠持有同步鎖掂墓。
比如在獨(dú)占模式下谦纱,我們可以把 state 的初始值設(shè)置成 0,當(dāng)某個(gè)線(xiàn)程申請(qǐng)鎖對(duì)象時(shí)君编,需要判斷 state 的值是不是 0跨嘉,如果不是 0 的話(huà)意味著其他線(xiàn)程已經(jīng)持有該鎖,則本線(xiàn)程需要阻塞等待吃嘿。
共享模式:可以有多個(gè)線(xiàn)程持有同步鎖祠乃。
在共享模式下的道理也差不多,比如說(shuō)某項(xiàng)操作我們?cè)试S 10 個(gè)線(xiàn)程同時(shí)進(jìn)行兑燥,超過(guò)這個(gè)數(shù)量的線(xiàn)程就需要阻塞等待亮瓷。那么只需要在線(xiàn)程申請(qǐng)對(duì)象時(shí)判斷 state 的值是否小于 10。如果小于 10贪嫂,就將 state 加 1 后繼續(xù)同步語(yǔ)句的執(zhí)行寺庄;如果等于 10,說(shuō)明已經(jīng)有 10 個(gè)線(xiàn)程在同時(shí)執(zhí)行該操作力崇,本線(xiàn)程需要阻塞等待斗塘。
Node 雙端隊(duì)列節(jié)點(diǎn)
Node 是一個(gè)先進(jìn)先出的雙端隊(duì)列,并且是等待隊(duì)列亮靴,當(dāng)多線(xiàn)程爭(zhēng)用資源被阻塞時(shí)會(huì)進(jìn)入此隊(duì)列馍盟。這個(gè)隊(duì)列是 AQS 實(shí)現(xiàn)多線(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ì)象的線(xiàn)程執(zhí)行同步代碼瞄桨,多個(gè)線(xiàn)程競(jìng)爭(zhēng)鎖時(shí)话速,競(jìng)爭(zhēng)失敗的線(xiàn)程需要被阻塞等待后續(xù)喚醒。那么 ReentrantLock 是如何實(shí)現(xiàn)讓線(xiàn)程等待并喚醒的呢芯侥?
前面中我們提到在 ReentrantLock.lock() 階段泊交,在 acquire() 方法中會(huì)先后調(diào)用 tryAcquire、addWaiter柱查、acquireQueued 這 3 個(gè)方法來(lái)處理廓俭。tryAcquire 在 ReentrantLock 中被復(fù)寫(xiě)并實(shí)現(xiàn),如果返回 true 說(shuō)明成功獲取鎖唉工,就繼續(xù)執(zhí)行同步代碼語(yǔ)句研乒。可是如果 tryAcquire 返回 false淋硝,也就是當(dāng)前鎖對(duì)象被其他線(xiàn)程所持有雹熬,那么當(dāng)前線(xiàn)程會(huì)被 AQS 如何處理呢?
addWaiter
首先當(dāng)前獲取鎖失敗的線(xiàn)程會(huì)被添加到一個(gè)等待隊(duì)列的末端奖地,具體源碼如下:
有兩種情況會(huì)致使插入隊(duì)列失旈匣!:
?1.tail 為空:說(shuō)明隊(duì)列從未初始化,因此需要調(diào)用 enq 方法在隊(duì)列中插入一個(gè)空的 Node参歹;
?2.compareAndSetTail 失斞龀:說(shuō)明插入過(guò)程中有線(xiàn)程修改了此隊(duì)列,因此需要調(diào)用 enq 將當(dāng)前 node 重新插入到隊(duì)列末端犬庇。
經(jīng)過(guò) addWaiter 方法之后僧界,此時(shí)線(xiàn)程以 Node 的方式被加入到隊(duì)列的末端,但是線(xiàn)程還沒(méi)有被執(zhí)行阻塞操作臭挽,真正的阻塞操作是在下面的 acquireQueued 方法中判斷執(zhí)行捂襟。
acquireQueued
在 acquireQueued 方法中并不會(huì)立即掛起該節(jié)點(diǎn)中的線(xiàn)程,因此在插入節(jié)點(diǎn)的過(guò)程中欢峰,之前持有鎖的線(xiàn)程可能已經(jīng)執(zhí)行完畢并釋放鎖葬荷,所以這里使用自旋再次去嘗試獲取鎖(不放過(guò)任何優(yōu)化細(xì)節(jié))。如果自旋操作還是沒(méi)有獲取到鎖纽帖!那么就將該線(xiàn)程掛起(阻塞)宠漩,該方法的源碼如下:
可以看出在 shouldParkAfterFailedAcquire 方法中會(huì)判讀當(dāng)前線(xiàn)程是否應(yīng)該被掛起,其代碼如下:
首先獲取前驅(qū)節(jié)點(diǎn)的 waitStatus 值懊直,Node 中的 waitStatus 一共有 5 種取值扒吁,分別代表的意義如下:
waitStatue值描述
? 1.CANCELLED (1) 當(dāng)前線(xiàn)程因?yàn)槌瑫r(shí)或者中斷被取消。這是一個(gè)終結(jié)態(tài)室囊,也就是狀態(tài)到此為止
? 2.SIGNAL (-1) 當(dāng)前線(xiàn)程的后繼線(xiàn)程被阻塞或者即將被阻塞雕崩,當(dāng)前線(xiàn)程釋放鎖或者取消后需要喚醒后繼線(xiàn)程魁索。這個(gè)狀態(tài)一般都是后繼線(xiàn)程來(lái)設(shè)置前驅(qū)節(jié)點(diǎn)的
? 3.CONDITION (-2) 當(dāng)前線(xiàn)程在 condition 隊(duì)列中
? 4.PROPAGATE (-3) 用于將喚醒后繼線(xiàn)程傳遞下去,這個(gè)狀態(tài)的引入是為了完善和增強(qiáng)共享鎖的喚醒機(jī)制盼铁。在一個(gè)節(jié)點(diǎn)成為頭節(jié)點(diǎn)之前粗蔚,是不會(huì)躍遷為此狀態(tài)的
? 5. 0 表示無(wú)鎖狀態(tài)
接下來(lái)根據(jù) waitStatus 不同的值進(jìn)行不同的操作,主要有以下幾種情況:
?1.如果 waitStatus 等于 SIGNAL捉貌,返回 true 將當(dāng)前線(xiàn)程掛起支鸡,等待后續(xù)喚醒操作即可。
?2.如果 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)急前。
?3.如果 waitStatus 既不是 SIGNAL 也不是 CANCEL醒陆,則將當(dāng)前節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)狀態(tài)設(shè)置為 SIGNAL,這樣做的好處是下一次執(zhí)行 shouldParkAfterFailedAcquire 時(shí)可以直接返回 true裆针,掛起線(xiàn)程刨摩。
代碼再回到 acquireQueued 中,如果 shouldParkAfterFailedAcquire 返回 true 表示線(xiàn)程需要被掛起世吨,那么會(huì)繼續(xù)調(diào)用 parkAndCheckInterrupt 方法執(zhí)行真正的阻塞線(xiàn)程代碼澡刹,具體如下:
這個(gè)方法比較簡(jiǎn)單,只是調(diào)用了 LockSupport 中的 park 方法耘婚。在 LockSupport.park() 方法中調(diào)用了 Unsafe API 來(lái)執(zhí)行底層 native 方法將線(xiàn)程掛起罢浇,代碼到這已經(jīng)到了操作系統(tǒng)的層面,沒(méi)有必要再深入分析沐祷。
至此嚷闭,獲取鎖的大體流程已經(jīng)分析完畢,總結(jié)一下整個(gè)過(guò)程如下:
?1.AQS 的模板方法 acquire 通過(guò)調(diào)用子類(lèi)自定義實(shí)現(xiàn)的 tryAcquire 獲取鎖赖临;
?2.如果獲取鎖失敗胞锰,通過(guò) addWaiter 方法將線(xiàn)程構(gòu)造成 Node 節(jié)點(diǎn)插入到同步隊(duì)列隊(duì)尾;
?3.在 acquirQueued 方法中以自旋的方法嘗試獲取鎖兢榨,如果失敗則判斷是否需要將當(dāng)前線(xiàn)程阻塞嗅榕,如果需要阻塞則最終執(zhí)行 LockSupport(Unsafe) 中的 native API 來(lái)實(shí)現(xiàn)線(xiàn)程阻塞。
釋放鎖流程分析
在上面加鎖階段被阻塞的線(xiàn)程需要被喚醒過(guò)后才可以重新執(zhí)行吵聪。那具體 AQS 是何時(shí)嘗試喚醒等待隊(duì)列中被阻塞的線(xiàn)程呢凌那?
同加鎖過(guò)程一樣,釋放鎖需要從 ReentrantLock.unlock() 方法開(kāi)始:
可以看出暖璧,首先調(diào)用 tryRelease 方法嘗試釋放鎖案怯,如果成功最終會(huì)調(diào)用 AQS 中的 unparkSuccessor 方法來(lái)實(shí)現(xiàn)釋放鎖的操作。unparkSuccessor 的具體實(shí)現(xiàn)如下:
解釋說(shuō)明:
首先獲取當(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ì)列的尾部開(kāi)始遍歷,直到尋找第一個(gè) waitStatus 小于 0 的節(jié)點(diǎn)麦锯。
如果最終遍歷到的節(jié)點(diǎn)不為 null恕稠,再調(diào)用 LockSupport.unpark 方法,調(diào)用底層方法喚醒線(xiàn)程扶欣。 至此鹅巍,線(xiàn)程被喚醒的時(shí)機(jī)也分析完畢。
不得不說(shuō)的 CAS
不管是在加鎖還是釋放鎖階段料祠,多次提到了一種通用的操作:compareAndSetXXX骆捧。這種操作最終會(huì)調(diào)用 Unsafe 中的 API 進(jìn)行 CAS 操作。
CAS 全稱(chēng)是 Compare And Swap髓绽,譯為比較和替換敛苇,是一種通過(guò)硬件實(shí)現(xiàn)并發(fā)安全的常用技術(shù),底層通過(guò)利用 CPU 的 CAS 指令對(duì)緩存加鎖或總線(xiàn)加鎖的方式來(lái)實(shí)現(xiàn)多處理器之間的原子操作顺呕。
它的實(shí)現(xiàn)過(guò)程主要有 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)和處理器的不同來(lái)選擇對(duì)應(yīng)的調(diào)用代碼蹦掐,以 Windows 和 X86 處理器為例,如果是多處理器驰徊,通過(guò)帶 lock 前綴的 cmpxchg 指令對(duì)緩存加鎖或總線(xiàn)加鎖的方式來(lái)實(shí)現(xiàn)多處理器之間的原子操作笤闯;如果是單處理器,通過(guò) cmpxchg 指令完成原子操作棍厂。
而我通俗理解為通過(guò)CPU對(duì)更改的值進(jìn)行原子操作颗味,當(dāng)且僅當(dāng)預(yù)期值 E(舊的值)和內(nèi)存值 V (當(dāng)前值)相同時(shí),指才能被修改成新值 U牺弹。如果不相同則不能更改浦马,結(jié)果為此次更改失敗,等待下一次更改张漂。這樣保證了只有一個(gè)操作能修改成功晶默,保證原子性。這里等待下次操作是進(jìn)入自旋航攒。
最后
總體來(lái)說(shuō)磺陡,AQS 是一套框架,在框架內(nèi)部已經(jīng)封裝好了大部分同步需要的邏輯,在 AQS 內(nèi)部維護(hù)了一個(gè)狀態(tài)指示器 state 和一個(gè)等待隊(duì)列 Node币他,而通過(guò) state 的操作又分為兩種:獨(dú)占式和共享式坞靶,這就導(dǎo)致 AQS 有兩種不同的實(shí)現(xiàn):獨(dú)占鎖(ReentrantLock 等)和分享鎖(CountDownLatch、讀寫(xiě)鎖等)蝴悉。本課時(shí)主要從獨(dú)占鎖的角度深入分析了 AQS 的加鎖和釋放鎖的流程彰阴。
理解 AQS 的原理對(duì)理解 JUC 包中其他組件實(shí)現(xiàn)的基礎(chǔ)有幫助,并且理解其原理才能更好的擴(kuò)展其功能拍冠。上層開(kāi)發(fā)人員可以基于此框架基礎(chǔ)上進(jìn)行擴(kuò)展實(shí)現(xiàn)適合不同場(chǎng)景尿这、不同功能的鎖。其中幾個(gè)有可能需要子類(lèi)同步器實(shí)現(xiàn)的方法如下庆杜。
?1.lock()射众。
?2.tryAcquire(int):獨(dú)占方式。嘗試獲取資源晃财,成功則返回 true责球,失敗則返回 false。
?3.tryRelease(int):獨(dú)占方式拓劝。嘗試釋放資源,成功則返回 true嘉裤,失敗則返回 false郑临。
?4.tryAcquireShared(int):共享方式。嘗試獲取資源屑宠。負(fù)數(shù)表示失斚岫础;0 表示成功典奉,但沒(méi)有剩余可用資源躺翻;正數(shù)表示成功,且有剩余資源卫玖。
?5.tryReleaseShared(int):共享方式公你。嘗試釋放資源,如果釋放后允許喚醒后續(xù)等待結(jié)點(diǎn)返回 true假瞬,否則返回 false陕靠。