十年網(wǎng)站開發(fā)經(jīng)驗(yàn) + 多家企業(yè)客戶 + 靠譜的建站團(tuán)隊(duì)
量身定制 + 運(yùn)營(yíng)維護(hù)+專業(yè)推廣+無憂售后,網(wǎng)站問題一站解決
這篇文章給大家分享的是有關(guān)Java并發(fā)中AQS原理的示例分析的內(nèi)容。小編覺得挺實(shí)用的,因此分享給大家做個(gè)參考,一起跟隨小編過來看看吧。

1、線程阻塞原語(yǔ)
Java 的線程阻塞和喚醒是通過 Unsafe 類的 park 和 unpark 方法做到的。
這兩個(gè)方法都是 native 方法,它們本身是由 C 語(yǔ)言來實(shí)現(xiàn)的核心功能。park 的意思是停車,讓當(dāng)前運(yùn)行的線程 Thread.currentThread() 休眠,unpark 的意思是解除停車,喚醒指定線程。
這兩個(gè)方法在底層是使用操作系統(tǒng)提供的信號(hào)量機(jī)制來實(shí)現(xiàn)的。具體實(shí)現(xiàn)過程要深究 C 代碼,這里暫時(shí)不去具體分析。park 方法的兩個(gè)參數(shù)用來控制休眠多長(zhǎng)時(shí)間,第一個(gè)參數(shù) isAbsolute 表示第二個(gè)參數(shù)是絕對(duì)時(shí)間還是相對(duì)時(shí)間,單位是毫秒。
線程從啟動(dòng)開始就會(huì)一直跑,除了操作系統(tǒng)的任務(wù)調(diào)度策略外,它只有在調(diào)用 park 的時(shí)候才會(huì)暫停運(yùn)行。鎖可以暫停線程的奧秘所在正是因?yàn)殒i在底層調(diào)用了 park 方法。
2、parkBlocker
線程對(duì)象 Thread 里面有一個(gè)重要的屬性 parkBlocker,它保存當(dāng)前線程因?yàn)槭裁炊? park。就好比停車場(chǎng)上停了很多車,這些車主都是來參加一場(chǎng)拍賣會(huì)的,等拍下自己想要的物品后,就把車開走。那么這里的 parkBlocker 大約就是指這場(chǎng)「拍賣會(huì)」。它是一系列沖突線程的管理者協(xié)調(diào)者,哪個(gè)線程該休眠該喚醒都是由它來控制的。
當(dāng)線程被 unpark 喚醒后,這個(gè)屬性會(huì)被置為 null。Unsafe.park 和 unpark 并不會(huì)幫我們?cè)O(shè)置 parkBlocker 屬性,負(fù)責(zé)管理這個(gè)屬性的工具類是 LockSupport,它對(duì) Unsafe 這兩個(gè)方法進(jìn)行了簡(jiǎn)單的包裝。
Java 的鎖數(shù)據(jù)結(jié)構(gòu)正是通過調(diào)用 LockSupport 來實(shí)現(xiàn)休眠與喚醒的。線程對(duì)象里面的 parkBlocker 字段的值就是下面我們要講的「排隊(duì)管理器」。
3、排隊(duì)管理器
當(dāng)多個(gè)線程爭(zhēng)用同一把鎖時(shí),必須有排隊(duì)機(jī)制將那些沒能拿到鎖的線程串在一起。當(dāng)鎖釋放時(shí),鎖管理器就會(huì)挑選一個(gè)合適的線程來占有這個(gè)剛剛釋放的鎖。
每一把鎖內(nèi)部都會(huì)有這樣一個(gè)隊(duì)列管理器,管理器里面會(huì)維護(hù)一個(gè)等待的線程隊(duì)列。ReentrantLock 里面的隊(duì)列管理器是 AbstractQueuedSynchronizer,它內(nèi)部的等待隊(duì)列是一個(gè)雙向列表結(jié)構(gòu)。
加鎖不成功時(shí),當(dāng)前的線程就會(huì)把自己納入到等待鏈表的尾部,然后調(diào)用 LockSupport.park 將自己休眠。其它線程解鎖時(shí),會(huì)從鏈表的表頭取一個(gè)節(jié)點(diǎn),調(diào)用 LockSupport.unpark 喚醒它。
AbstractQueuedSynchronizer 類是一個(gè)抽象類,它是所有的鎖隊(duì)列管理器的父類,JDK 中的各種形式的鎖其內(nèi)部的隊(duì)列管理器都繼承了這個(gè)類,它是 Java 并發(fā)世界的核心基石。
比如 ReentrantLock、ReadWriteLock、CountDownLatch、Semaphore、ThreadPoolExecutor 內(nèi)部的隊(duì)列管理器都是它的子類。這個(gè)抽象類暴露了一些抽象方法,每一種鎖都需要對(duì)這個(gè)管理器進(jìn)行定制。而 JDK 內(nèi)置的所有并發(fā)數(shù)據(jù)結(jié)構(gòu)都是在這些鎖的保護(hù)下完成的,它是JDK 多線程高樓大廈的地基。
鎖管理器維護(hù)的只是一個(gè)普通的雙向列表形式的隊(duì)列,這個(gè)數(shù)據(jù)結(jié)構(gòu)很簡(jiǎn)單,但是仔細(xì)維護(hù)起來卻相當(dāng)復(fù)雜,因?yàn)樗枰?xì)考慮多線程并發(fā)問題,每一行代碼都寫的無比小心。
JDK 鎖管理器的實(shí)現(xiàn)者是 Douglas S. Lea,Java 并發(fā)包幾乎全是他單槍匹馬寫出來的,在算法的世界里越是精巧的東西越是適合一個(gè)人來做。
后面我們將 AbstractQueuedSynchronizer 簡(jiǎn)寫成 AQS。我必須提醒各位讀者,AQS 太復(fù)雜了,如果在理解它的路上遇到了挫折,這很正常。目前市場(chǎng)上并不存在一本可以輕松理解 AQS 的書籍,能夠吃透 AQS 的人太少太少,我自己也不算。
4、公平鎖與非公平鎖
公平鎖會(huì)確保請(qǐng)求鎖和獲得鎖的順序,如果在某個(gè)點(diǎn)鎖正處于自由狀態(tài),這時(shí)有一個(gè)線程要嘗試加鎖,公平鎖還必須查看當(dāng)前有沒有其它線程排在排隊(duì),而非公平鎖可以直接插隊(duì)。聯(lián)想一下在肯德基買漢堡時(shí)的排隊(duì)場(chǎng)景。
也許你會(huì)問,如果某個(gè)鎖處于自由狀態(tài),那它怎么會(huì)有排隊(duì)的線程呢?我們假設(shè)此刻持有鎖的線程剛剛釋放了鎖,它喚醒了等待隊(duì)列中第一個(gè)節(jié)點(diǎn)線程,這時(shí)候被喚醒的線程剛剛從 park 方法返回,接下來它就會(huì)嘗試去加鎖,那么從 park 返回到加鎖之間的狀態(tài)就是鎖的自由態(tài),這很短暫,而這短暫的時(shí)間內(nèi)還可能有其它線程也在嘗試加鎖。
其次還有一點(diǎn)需要注意,執(zhí)行了 Lock.park 方法的線程自我休眠后,并不是非要等到其它線程 unpark 了自己才會(huì)醒來,它可能隨時(shí)會(huì)以某種未知的原因醒來。我們看源碼注釋,park 返回的原因有四種:
①其它線程 unpark 了當(dāng)前線程;
②時(shí)間到了自然醒(park 有時(shí)間參數(shù));
③其它線程 interrupt 了當(dāng)前線程;
④其它未知原因?qū)е碌摹讣傩选?
文檔中沒有明確說明何種未知原因會(huì)導(dǎo)致假醒,它倒是說明了當(dāng) park 方法返回時(shí)并不意味著鎖自由了,醒過來的線程在重新嘗試獲取鎖失敗后將會(huì)再次 park 自己。所以加鎖的過程需要寫在一個(gè)循環(huán)里,在成功拿到鎖之前可能會(huì)進(jìn)行多次嘗試。
計(jì)算機(jī)世界非公平鎖的服務(wù)效率要高于公平鎖,所以 Java 默認(rèn)的鎖都使用了非公平鎖。不過現(xiàn)實(shí)世界似乎非公平鎖的效率會(huì)差一點(diǎn),比如在肯德基如果可以不停插隊(duì),你可以想象現(xiàn)場(chǎng)肯定一片混亂。為什么計(jì)算機(jī)世界和現(xiàn)實(shí)世界會(huì)有差異,大概是因?yàn)樵谟?jì)算機(jī)世界里某個(gè)線程插隊(duì)并不會(huì)導(dǎo)致其它線程抱怨。
5、共享鎖與排他鎖
ReentrantLock 的鎖是排他鎖,一個(gè)線程持有,其它線程都必須等待。而 ReadWriteLock 里面的讀鎖不是排他鎖,它允許多線程同時(shí)持有讀鎖,這是共享鎖。共享鎖和排他鎖是通過 Node 類里面的 nextWaiter 字段區(qū)分的。
那為什么這個(gè)字段沒有命名成 mode 或者 type 或者干脆直接叫 shared?這是因?yàn)?nextWaiter 在其它場(chǎng)景還有不一樣的用途,它就像 C 語(yǔ)言聯(lián)合類型的字段一樣隨機(jī)應(yīng)變,只不過 Java 語(yǔ)言沒有聯(lián)合類型。
6、條件變量
關(guān)于條件變量,需要提出的第一個(gè)問題是為什么需要條件變量,只有鎖還不夠么?考慮下面的偽代碼,當(dāng)某個(gè)條件滿足時(shí),才去干某件事
當(dāng)條件不滿足時(shí),就循環(huán)重試(其它線程會(huì)通過加鎖來修改條件),但是需要間隔 sleep,不然 CPU 就會(huì)因?yàn)榭辙D(zhuǎn)而飆高。這里存在一個(gè)問題,那就是 sleep 多久不好控制。間隔太久,會(huì)拖慢整體效率,甚至?xí)e(cuò)過時(shí)機(jī)(條件瞬間滿足了又立即被重置了),間隔太短,又回導(dǎo)致 CPU 空轉(zhuǎn)。有了條件變量,這個(gè)問題就可以解決了
await() 方法會(huì)一直阻塞在 cond 條件變量上直到被另外一個(gè)線程調(diào)用了 cond.signal() 或者 cond.signalAll() 方法后才會(huì)返回,await() 阻塞時(shí)會(huì)自動(dòng)釋放當(dāng)前線程持有的鎖,await() 被喚醒后會(huì)再次嘗試持有鎖(可能又需要排隊(duì)),拿到鎖成功之后 await() 方法才能成功返回。
阻塞在條件變量上的線程可以有多個(gè),這些阻塞線程會(huì)被串聯(lián)成一個(gè)條件等待隊(duì)列。當(dāng) signalAll() 被調(diào)用時(shí),會(huì)喚醒所有的阻塞線程,讓所有的阻塞線程重新開始爭(zhēng)搶鎖。如果調(diào)用的是 signal() 只會(huì)喚醒隊(duì)列頭部的線程,這樣可以避免「驚群?jiǎn)栴}」。
await() 方法必須立即釋放鎖,否則臨界區(qū)狀態(tài)就不能被其它線程修改,condition_is_true() 返回的結(jié)果也就不會(huì)改變。 這也是為什么條件變量必須由鎖對(duì)象來創(chuàng)建,條件變量需要持有鎖對(duì)象的引用這樣才可以釋放鎖以及被 signal 喚醒后重新加鎖。
創(chuàng)建條件變量的鎖必須是排他鎖,如果是共享鎖被 await() 方法釋放了并不能保證臨界區(qū)的狀態(tài)可以被其它線程來修改,可以修改臨界區(qū)狀態(tài)的只能是排他鎖。
有了條件變量,sleep 不好控制的問題就解決了。當(dāng)條件滿足時(shí),調(diào)用 signal() 或者 signalAll() 方法,阻塞的線程可以立即被喚醒,幾乎沒有任何延遲。
7、條件等待隊(duì)列
當(dāng)多個(gè)線程 await() 在同一個(gè)條件變量上時(shí),會(huì)形成一個(gè)條件等待隊(duì)列。同一個(gè)鎖可以創(chuàng)建多個(gè)條件變量,就會(huì)存在多個(gè)條件等待隊(duì)列。這個(gè)隊(duì)列和 AQS 的隊(duì)列結(jié)構(gòu)很接近,只不過它不是雙向隊(duì)列,而是單向隊(duì)列。隊(duì)列中的節(jié)點(diǎn)和 AQS 等待隊(duì)列的節(jié)點(diǎn)是同一個(gè)類,但是節(jié)點(diǎn)指針不是 prev 和 next,而是 nextWaiter。
ConditionObject 是 AQS 的內(nèi)部類,這個(gè)對(duì)象里會(huì)有一個(gè)隱藏的指針 this$0 指向外部的 AQS 對(duì)象,ConditionObject 可以直接訪問 AQS 對(duì)象的所有屬性和方法(加鎖解鎖)。位于條件等待隊(duì)列里的所有節(jié)點(diǎn)的 waitStatus 狀態(tài)都被標(biāo)記為 CONDITION,表示節(jié)點(diǎn)是因?yàn)闂l件變量而等待。
8、隊(duì)列轉(zhuǎn)移
當(dāng)條件變量的 signal() 方法被調(diào)用時(shí),條件等待隊(duì)列的頭節(jié)點(diǎn)線程會(huì)被喚醒,該節(jié)點(diǎn)從條件等待隊(duì)列中被摘走,然后被轉(zhuǎn)移到 AQS 的等待隊(duì)列中,準(zhǔn)備排隊(duì)嘗試重新獲取鎖。這時(shí)節(jié)點(diǎn)的狀態(tài)從 CONDITION 轉(zhuǎn)為 SIGNAL,表示當(dāng)前節(jié)點(diǎn)是被條件變量喚醒轉(zhuǎn)移過來的。
被轉(zhuǎn)移的節(jié)點(diǎn)的 nextWaiter 字段的含義也發(fā)生了變更,在條件隊(duì)列里它是下一個(gè)節(jié)點(diǎn)的指針,在 AQS 等待隊(duì)列里它是共享鎖還是互斥鎖的標(biāo)志。
9、讀寫鎖
讀寫鎖分為兩個(gè)鎖對(duì)象 ReadLock 和 WriteLock,這兩個(gè)鎖對(duì)象共享同一個(gè) AQS。AQS 的鎖計(jì)數(shù)變量 state 將分為兩個(gè)部分,前 16bit 為共享鎖 ReadLock 計(jì)數(shù),后 16bit 為互斥鎖 WriteLock 計(jì)數(shù)?;コ怄i記錄的是當(dāng)前寫鎖重入的次數(shù),共享鎖記錄的是所有當(dāng)前持有共享讀鎖的線程重入總次數(shù)。
讀寫鎖同樣也需要考慮公平鎖和非公平鎖。共享鎖和互斥鎖的公平鎖策略和 ReentrantLock 一樣,就是看看當(dāng)前還有沒有其它線程在排隊(duì),自己會(huì)乖乖排到隊(duì)尾。非公平鎖策略不一樣,它會(huì)比較偏向于給寫鎖提供更多的機(jī)會(huì)。
如果當(dāng)前 AQS 隊(duì)列里有任何讀寫請(qǐng)求的線程在排隊(duì),那么寫鎖可以直接去爭(zhēng)搶,但是如果隊(duì)頭是寫鎖請(qǐng)求,那么讀鎖需要將機(jī)會(huì)讓給寫鎖,去隊(duì)尾排隊(duì)。 畢竟讀寫鎖適合讀多寫少的場(chǎng)合,對(duì)于偶爾出現(xiàn)一個(gè)寫鎖請(qǐng)求就應(yīng)該得到更高的優(yōu)先級(jí)去處理。
感謝各位的閱讀!關(guān)于“Java并發(fā)中AQS原理的示例分析”這篇文章就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,讓大家可以學(xué)到更多知識(shí),如果覺得文章不錯(cuò),可以把它分享出去讓更多的人看到吧!