來源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è)主要步驟:
- tryAcquire() 方法主要目的是嘗試獲取鎖;
- addWaiter() 如果 tryAcquire() 嘗試獲取鎖失敗則調(diào)用 addWaiter 將當(dāng)前線程添加到一個(gè)等待隊(duì)列中礁遵;
- 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梦谜。