在之前介紹AQS源碼的時(shí)候,還遺留了一個(gè)內(nèi)部類ConditionObject沒有介紹,它也是并發(fā)中至關(guān)重要的類。
主要作用
Condition主要提供多個(gè)await方法以及signal、signalAll方法,對(duì)標(biāo)的是Object的wait、notify、notifyAll方法,對(duì)應(yīng)的作用也一樣。
ConditionObject是AQS的內(nèi)部類并且實(shí)現(xiàn)了接口Condition,主要提供的功能也一樣。
創(chuàng)建方法
只有ReentrantLock提供了獲取AQS的Condition對(duì)象的方法。首先要new一個(gè)ReentrantLock對(duì)象,然后通過ReentrantLock的newCondition()方法獲取Condition對(duì)象,每次獲取都是創(chuàng)建一個(gè)新的Condition對(duì)象。
結(jié)構(gòu)與主要方法
Condition(表示AQS中的ConditionObject,下同)只有兩個(gè)屬性firstWaiter、lastWaiter,類型是AQS的Node,也就是Condition也維護(hù)一個(gè)鏈表,firstWaiter、lastWaiter分別是鏈表的首位節(jié)點(diǎn)。
await方法源碼分析
Condition提供了很多個(gè)await方法,主要區(qū)別在于是否在如果等待一定時(shí)間后不是否繼續(xù)等待,這里分析下await最基礎(chǔ)的無(wú)參方法,源代碼流程圖如下圖:

簡(jiǎn)單分析下await方法:
首先創(chuàng)建一個(gè)Node(當(dāng)前線程,狀態(tài)-2)節(jié)點(diǎn)并加到等待鏈表的尾部,這個(gè)Node是AQS中的Node,也會(huì)保存當(dāng)前線程;
第二步是釋放鎖,因?yàn)榫€程在等待過程中其他線程也需要使用lock,所以這里先釋放鎖,也就是修改AQS中state的值。
第三步是進(jìn)入一個(gè)while循環(huán),跳出循環(huán)判斷的條件是節(jié)點(diǎn)狀態(tài)不等于-2、并且要在同步隊(duì)列中(在AQS維護(hù)的鏈表中),如果沒在同步隊(duì)列中則會(huì)進(jìn)入循環(huán)掛起線程,當(dāng)再次被喚醒的時(shí)候會(huì)優(yōu)先判斷線程是否中斷,如果線程中斷也會(huì)跳出循環(huán),否則繼續(xù)循環(huán)判斷。
第四步是當(dāng)當(dāng)前線程跳出循環(huán)時(shí),會(huì)重新設(shè)置鎖,設(shè)置成功再去根據(jù)情況整理鏈表,如果線程是中斷的,則根據(jù)情況拋出中斷異常,或者中斷。
從源碼中可以得出,在調(diào)用await方法之前一定要先獲取到鎖,否則會(huì)拋出異常。
signal方法:讓線程跳出循環(huán)
知道了await方法的源碼流程,就可以猜測(cè)signal方法的源碼了,先來(lái)分析下await方法如何才能執(zhí)行完,先不考慮中斷線程的情況,要讓線程跳出while循環(huán)需要兩個(gè)條件:節(jié)點(diǎn)狀態(tài)不能是-2、讓節(jié)點(diǎn)在同步隊(duì)列中;記住這里的同步隊(duì)列是AQS維護(hù)的同步隊(duì)列。
所以現(xiàn)在再來(lái)看signal方法的源碼就簡(jiǎn)單了:
首先是拿到firstWaiter,如果為null則?繼續(xù)往鏈表后面找;
第二步是把firstWaiter的狀態(tài)從等待狀態(tài)改為0,如果修改失敗則直接返回false,說(shuō)明這個(gè)節(jié)點(diǎn)以及被修改成其他狀態(tài)了,繼續(xù)尋找下一個(gè)節(jié)點(diǎn)。
第三步把節(jié)點(diǎn)加到同步隊(duì)列的尾部;
第四步根據(jù)同步隊(duì)列的前置節(jié)點(diǎn)狀態(tài)判斷是否需要直接喚醒當(dāng)前節(jié)點(diǎn)中的線程;
signal方法就是把Condition中維護(hù)的鏈表節(jié)點(diǎn)的頭部節(jié)點(diǎn)狀態(tài)設(shè)置為0并且加到AQS的同步隊(duì)列中。signalAll方法就是把鏈表的所有節(jié)點(diǎn)走一下signal方法的流程。
Condition與Object
分析了Condition方法但是并沒有體現(xiàn)它相比于Object的wait、notify、notifyAll的優(yōu)勢(shì),實(shí)際上這個(gè)問題在之前的文章設(shè)計(jì)阻塞隊(duì)列中有對(duì)比,這里大概講一下,阻塞隊(duì)列有put與take方法,當(dāng)隊(duì)列滿了put方法會(huì)阻塞,當(dāng)隊(duì)列是空時(shí)take方法會(huì)阻塞。
用Object的wait、notifyAll方法來(lái)實(shí)現(xiàn)put、take方法的阻塞,在多線程情況下一個(gè)put方法獲取到鎖,那么所有的包括put方法和take方法都會(huì)阻塞,當(dāng)put方法執(zhí)行完成后會(huì)喚醒take線程,但同時(shí)put線程也會(huì)被喚醒來(lái)?yè)寛?zhí)行權(quán),但是如果此時(shí)隊(duì)列已滿,實(shí)際上所有的put線程都不應(yīng)該會(huì)喚醒。
如果采用ReentrantLock來(lái)new出兩個(gè)Condition來(lái)實(shí)現(xiàn),put方法如果發(fā)現(xiàn)隊(duì)列滿了則調(diào)用一個(gè)Condition的await方法來(lái)阻塞線程,另外一個(gè)來(lái)阻塞take方法,當(dāng)put方法執(zhí)行完后只用調(diào)用阻塞take方法中的Condition的signal,只用喚醒隊(duì)列中最前面一個(gè)就夠了。
總的來(lái)說(shuō)可以利用一個(gè)ReentrantLock來(lái)實(shí)現(xiàn)對(duì)資源的訪問控制,利用多個(gè)Condition來(lái)更加精準(zhǔn)的控制線程的阻塞與喚醒,相比利用一個(gè)Object來(lái)控制更加準(zhǔn)確,可以使程序運(yùn)行的更加高效。
總結(jié)
最后再梳理下Condition的await、signal方法,再使用await方法之前必須調(diào)用Condition對(duì)應(yīng)的ReentrantLock的lock方法,也就是必須要獲取鎖。線程要跳出await方法就必須把節(jié)點(diǎn)放到AQS中的同步隊(duì)列中。而signal方法就是把節(jié)點(diǎn)放到同步隊(duì)列中。
再整理下整個(gè)流程:lock方法直到獲取鎖、調(diào)用await方法(生成一個(gè)Node放到Condition鏈表的末尾)、然后釋放鎖、進(jìn)入while循環(huán)掛起線程、另外的線程獲取到鎖調(diào)用signal修改Condition鏈表的頭部節(jié)點(diǎn)狀態(tài)并放到AQS同步隊(duì)列中、當(dāng)其他線程釋放鎖會(huì)去喚醒AQS同步隊(duì)列頭部線程、如果這個(gè)節(jié)點(diǎn)被喚醒就繼續(xù)執(zhí)行await的while判斷會(huì)跳出循環(huán)、設(shè)置鎖狀態(tài)await方法執(zhí)行完成、最后再調(diào)用unlock方法釋放鎖。
ReentrantLock用來(lái)維護(hù)同步隊(duì)列保證只有一個(gè)線程訪問資源、await方法把保存線程的節(jié)點(diǎn)加到Condition維護(hù)的鏈表中,signal方法把節(jié)點(diǎn)加到同步隊(duì)列中,通過這樣無(wú)論是await方法被喚醒還是其他地方ReentrantLock調(diào)用了lock都能保證資源只被最多一個(gè)線程訪問。
ReentrantLock與Condition在其他并發(fā)類中經(jīng)常遇到,值得弄得他們的原理與優(yōu)勢(shì),便于理解其他并發(fā)類。
Java程序員日常學(xué)習(xí)筆記,如理解有誤歡迎各位交流討論!
