更多多線程API解析請(qǐng)參考jdk8 版本并發(fā)源碼解讀
前言
Java中的FutureTask作為可異步執(zhí)行任務(wù)并可獲取執(zhí)行結(jié)果而被大家所熟知。通晨林ǎ可以使用future.get()來(lái)獲取線程的執(zhí)行結(jié)果酪术,在線程執(zhí)行結(jié)束之前,get方法會(huì)一直阻塞狀態(tài)翠储,直到call()返回绘雁,其優(yōu)點(diǎn)是使用線程異步執(zhí)行任務(wù)的情況下還可以獲取到線程的執(zhí)行結(jié)果,但是FutureTask的以上功能卻是依靠通過(guò)一個(gè)叫AbstractQueuedSynchronizer的類來(lái)實(shí)現(xiàn)援所,至少在JDK 1.5庐舟、JDK1.6版本是這樣的(從1.7開(kāi)始FutureTask已經(jīng)被其作者Doug Lea修改為不再依賴AbstractQueuedSynchronizer實(shí)現(xiàn)了,這是JDK1.7的變化之一)住拭。但是AbstractQueuedSynchronizer在JDK1.8中還有如下圖所示的眾多子類:
這些JDK中的工具類或多或少都被大家用過(guò)不止一次挪略,比如ReentrantLock历帚,我們知道ReentrantLock的功能是實(shí)現(xiàn)代碼段的并發(fā)訪問(wèn)控制,也就是通常意義上所說(shuō)的鎖杠娱,在沒(méi)有看到AbstractQueuedSynchronizer前挽牢,可能會(huì)以為它的實(shí)現(xiàn)是通過(guò)類似于synchronized,通過(guò)對(duì)對(duì)象加鎖來(lái)實(shí)現(xiàn)的墨辛。但事實(shí)上它僅僅是一個(gè)工具類!沒(méi)有使用更“高級(jí)”的機(jī)器指令趴俘,不是關(guān)鍵字睹簇,也不依靠JDK編譯時(shí)的特殊處理,僅僅作為一個(gè)普普通通的類就完成了代碼塊的并發(fā)訪問(wèn)控制寥闪,這就更讓人疑問(wèn)它怎么實(shí)現(xiàn)的代碼塊的并發(fā)訪問(wèn)控制的了太惠。那就讓我們一起來(lái)仔細(xì)看下Doug Lea怎么去實(shí)現(xiàn)的這個(gè)鎖。為了方便疲憋,本文中使用AQS代替AbstractQueuedSynchronizer凿渊。
細(xì)說(shuō)AQS
在深入分析AQS之前,我想先從AQS的功能上說(shuō)明下AQS缚柳,站在使用者的角度埃脏,AQS的功能可以分為兩類:獨(dú)占功能和共享功能,它的所有子類中秋忙,要么實(shí)現(xiàn)并使用了它獨(dú)占功能的API彩掐,要么使用了共享鎖的功能,而不會(huì)同時(shí)使用兩套API灰追,即便是它最有名的子類ReentrantReadWriteLock堵幽,也是通過(guò)兩個(gè)內(nèi)部類:讀鎖和寫鎖,分別實(shí)現(xiàn)的兩套API來(lái)實(shí)現(xiàn)的弹澎,為什么這么做朴下,后面我們?cè)俜治觯侥壳盀橹箍噍铮覀冎恍枰靼譇QS在功能上有獨(dú)占控制和共享控制兩種功能即可殴胧。
獨(dú)占鎖
在真正對(duì)解讀AQS之前,我想先從使用了它獨(dú)占控制功能的子類ReentrantLock說(shuō)起佩迟,分析ReentrantLock的同時(shí)看一看AQS的實(shí)現(xiàn)溃肪,再推理出AQS獨(dú)特的設(shè)計(jì)思路和實(shí)現(xiàn)方式。最后音五,再看其共享控制功能的實(shí)現(xiàn)惫撰。
對(duì)于ReentrantLock,使用過(guò)的同學(xué)應(yīng)該都知道躺涝,通常是這么用它的:
reentrantLock.lock()
//do something
reentrantLock.unlock()
ReentrantLock會(huì)保證 do something在同一時(shí)間只有一個(gè)線程在執(zhí)行這段代碼厨钻,或者說(shuō)扼雏,同一時(shí)刻只有一個(gè)線程的lock方法會(huì)返回。其余線程會(huì)被掛起夯膀,直到獲取鎖诗充。從這里可以看出,其實(shí)ReentrantLock實(shí)現(xiàn)的就是一個(gè)獨(dú)占鎖的功能:有且只有一個(gè)線程獲取到鎖诱建,其余線程全部掛起蝴蜓,直到該擁有鎖的線程釋放鎖,被掛起的線程被喚醒重新開(kāi)始競(jìng)爭(zhēng)鎖俺猿。沒(méi)錯(cuò)茎匠,ReentrantLock使用的就是AQS的獨(dú)占API實(shí)現(xiàn)的。
那現(xiàn)在我們就從ReentrantLock的實(shí)現(xiàn)開(kāi)始一起看看重入鎖是怎么實(shí)現(xiàn)的押袍。
首先看lock方法:
如FutureTask(JDK1.6)一樣诵冒,ReentrantLock內(nèi)部有代理類完成具體操作,ReentrantLock只是封裝了統(tǒng)一的一套API而已谊惭。值得注意的是汽馋,使用過(guò)ReentrantLock的同學(xué)應(yīng)該知道,ReentrantLock又分為公平鎖和非公平鎖圈盔,所以豹芯,ReentrantLock內(nèi)部只有兩個(gè)sync的實(shí)現(xiàn):
公平鎖:每個(gè)線程搶占鎖的順序?yàn)橄群笳{(diào)用lock方法的順序依次獲取鎖,類似于排隊(duì)吃飯驱敲。
非公平鎖:每個(gè)線程搶占鎖的順序不定告组,誰(shuí)運(yùn)氣好,誰(shuí)就獲取到鎖癌佩,和調(diào)用lock方法的先后順序無(wú)關(guān)木缝,類似于堵車時(shí),加塞的那些XXXX围辙。
到這里我碟,通過(guò)ReentrantLock的功能和鎖的所謂排不排隊(duì)的方式,我們是否可以這么猜測(cè)ReentrantLock或者AQS的實(shí)現(xiàn)(現(xiàn)在不清楚誰(shuí)去實(shí)現(xiàn)這些功能):有那么一個(gè)被volatile修飾的標(biāo)志位叫做key姚建,用來(lái)表示有沒(méi)有線程拿走了鎖矫俺,或者說(shuō),鎖還存不存在掸冤,還需要一個(gè)線程安全的隊(duì)列厘托,維護(hù)一堆被掛起的線程,以至于當(dāng)鎖被歸還時(shí)稿湿,能通知到這些被掛起的線程铅匹,可以來(lái)競(jìng)爭(zhēng)獲取鎖了。
至于公平鎖和非公平鎖饺藤,唯一的區(qū)別是在獲取鎖的時(shí)候是直接去獲取鎖包斑,還是進(jìn)入隊(duì)列排隊(duì)的問(wèn)題了流礁。為了驗(yàn)證我們的猜想,我們繼續(xù)看一下ReentrantLock中公平鎖的實(shí)現(xiàn):
調(diào)用到了AQS的acquire方法:
從方法名字上看語(yǔ)義是罗丰,嘗試獲取鎖神帅,獲取不到則創(chuàng)建一個(gè)waiter(當(dāng)前線程)后放到隊(duì)列中,這和我們猜測(cè)的好像很類似萌抵。
先看下tryAcquire方法:
留空了找御,Doug Lea是想留給子類去實(shí)現(xiàn)(既然要給子類實(shí)現(xiàn),應(yīng)該用抽象方法绍填,但是Doug Lea沒(méi)有這么做霎桅,原因是AQS有兩種功能,面向兩種使用場(chǎng)景沐兰,需要給子類定義的方法都是抽象方法了哆档,會(huì)導(dǎo)致子類無(wú)論如何都需要實(shí)現(xiàn)另外一種場(chǎng)景的抽象方法蔽挠,顯然住闯,這對(duì)子類來(lái)說(shuō)是不友好的。)
看下FairSync的tryAcquire方法:
getState方法是AQS的方法澳淑,因?yàn)樵贏QS里面有個(gè)叫statede的標(biāo)志位 :
事實(shí)上比原,這個(gè)state就是前面我們猜想的那個(gè)“key”!
回到tryAcquire方法:
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();//獲取當(dāng)前線程
int c = getState(); //獲取父類AQS中的標(biāo)志位
if (c == 0) {
if (!hasQueuedPredecessors() &&
//如果隊(duì)列中沒(méi)有其他線程 說(shuō)明沒(méi)有線程正在占有鎖杠巡!
compareAndSetState(0, acquires)) {
//修改一下?tīng)顟B(tài)位量窘,注意:這里的acquires是在lock的時(shí)候傳遞來(lái)的,從上面的圖中可以知道氢拥,這個(gè)值是寫死的1
setExclusiveOwnerThread(current);
//如果通過(guò)CAS操作將狀態(tài)為更新成功則代表當(dāng)前線程獲取鎖蚌铜,因此,將當(dāng)前線程設(shè)置到AQS的一個(gè)變量中嫩海,說(shuō)明這個(gè)線程拿走了鎖冬殃。
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
//如果不為0 意味著,鎖已經(jīng)被拿走了,但是,因?yàn)镽eentrantLock是重入鎖岳枷,
//是可以重復(fù)lock,unlock的稳强,只要成對(duì)出現(xiàn)行。一次斯稳。這里還要再判斷一次 獲取鎖的線程是不是當(dāng)前請(qǐng)求鎖的線程。
int nextc = c + acquires;//如果是的,累加在state字段上就可以了官册。
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
到此,如果如果獲取鎖难捌,tryAcquire返回true攀隔,反之皂贩,返回false,回到AQS的acquire方法昆汹。
如果沒(méi)有獲取到鎖明刷,按照我們的描述,應(yīng)該將當(dāng)前線程放到隊(duì)列中去满粗,只不過(guò)辈末,在放之前,需要做些包裝映皆。
先看addWaiter方法:
用當(dāng)前線程去構(gòu)造一個(gè)Node對(duì)象挤聘,mode是一個(gè)表示Node類型的字段,僅僅表示這個(gè)節(jié)點(diǎn)是獨(dú)占的捅彻,還是共享的组去,或者說(shuō),AQS的這個(gè)隊(duì)列中步淹,哪些節(jié)點(diǎn)是獨(dú)占的从隆,哪些是共享的。
這里lock調(diào)用的是AQS獨(dú)占的API缭裆,當(dāng)然键闺,可以寫死是獨(dú)占狀態(tài)的節(jié)點(diǎn)。
創(chuàng)建好節(jié)點(diǎn)后澈驼,將節(jié)點(diǎn)加入到隊(duì)列尾部辛燥,此處,在隊(duì)列不為空的時(shí)候缝其,先嘗試通過(guò)cas方式修改尾節(jié)點(diǎn)為最新的節(jié)點(diǎn)挎塌,如果修改失敗,意味著有并發(fā)内边,這個(gè)時(shí)候才會(huì)進(jìn)入enq中死循環(huán)榴都,“自旋”方式修改。
將線程的節(jié)點(diǎn)接入到隊(duì)里中后假残,當(dāng)然還需要做一件事:將當(dāng)前線程掛起缭贡!這個(gè)事,由acquireQueued來(lái)做辉懒。
在解釋acquireQueued之前阳惹,我們需要先看下AQS中隊(duì)列的內(nèi)存結(jié)構(gòu),我們知道眶俩,隊(duì)列由Node類型的節(jié)點(diǎn)組成莹汤,其中至少有兩個(gè)變量,一個(gè)封裝線程颠印,一個(gè)封裝節(jié)點(diǎn)類型纲岭。
而實(shí)際上抹竹,它的內(nèi)存結(jié)構(gòu)是這樣的(第一次節(jié)點(diǎn)插入時(shí),第一個(gè)節(jié)點(diǎn)是一個(gè)空節(jié)點(diǎn)止潮,代表有一個(gè)線程已經(jīng)獲取鎖窃判,事實(shí)上,隊(duì)列的第一個(gè)節(jié)點(diǎn)就是代表持有鎖的節(jié)點(diǎn)):
黃色節(jié)點(diǎn)為隊(duì)列默認(rèn)的頭節(jié)點(diǎn)喇闸,每次有線程競(jìng)爭(zhēng)失敗袄琳,進(jìn)入隊(duì)列后其實(shí)都是插入到隊(duì)列的尾節(jié)點(diǎn)(tail后面)后面。這個(gè)從enq方法可以看出來(lái)燃乍,上文中有提到enq方法為將節(jié)點(diǎn)插入隊(duì)列的方法:
再回來(lái)看看
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
//如果當(dāng)前的節(jié)點(diǎn)是head說(shuō)明他是隊(duì)列中第一個(gè)“有效的”節(jié)點(diǎn)唆樊,因此嘗試獲取,上文中有提到這個(gè)類是交給子類去擴(kuò)展的刻蟹。
setHead(node);//成功后逗旁,將上圖中的黃色節(jié)點(diǎn)移除,Node1變成頭節(jié)點(diǎn)舆瘪。
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
//否則片效,檢查前一個(gè)節(jié)點(diǎn)的狀態(tài)為,看當(dāng)前獲取鎖失敗的線程是否需要掛起介陶。
parkAndCheckInterrupt())
//如果需要堤舒,借助JUC包下的LockSopport類的靜態(tài)方法Park掛起當(dāng)前線程色建。知道被喚醒哺呜。
interrupted = true;
}
} finally {
if (failed) //如果有異常
cancelAcquire(node);// 取消請(qǐng)求,對(duì)應(yīng)到隊(duì)列操作箕戳,就是將當(dāng)前節(jié)點(diǎn)從隊(duì)列中移除某残。
}
}
這塊代碼有幾點(diǎn)需要說(shuō)明:
- Node節(jié)點(diǎn)中,除了存儲(chǔ)當(dāng)前線程陵吸,節(jié)點(diǎn)類型玻墅,隊(duì)列中前后元素的變量,還有一個(gè)叫waitStatus的變量壮虫,改變量用于描述節(jié)點(diǎn)的狀態(tài)澳厢,為什么需要這個(gè)狀態(tài)呢?
原因是:AQS的隊(duì)列中囚似,在有并發(fā)時(shí)剩拢,肯定會(huì)存取一定數(shù)量的節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)代表了一個(gè)線程的狀態(tài)饶唤,有的線程可能“等不及”獲取鎖了徐伐,需要放棄競(jìng)爭(zhēng),退出隊(duì)列募狂,有的線程在等待一些條件滿足办素,滿足后才恢復(fù)執(zhí)行(這里的描述很像某個(gè)J.U.C包下的工具類角雷,ReentrankLock的Condition,事實(shí)上性穿,Condition同樣也是AQS的子類)等等勺三,總之,各個(gè)線程有各個(gè)線程的狀態(tài)需曾,但總需要一個(gè)變量來(lái)描述它檩咱,這個(gè)變量就叫waitStatus,它有四種狀態(tài):
分別表示:
節(jié)點(diǎn)取消
節(jié)點(diǎn)等待觸發(fā)
節(jié)點(diǎn)等待條件
節(jié)點(diǎn)狀態(tài)需要向后傳播。
只有當(dāng)前節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn)為SIGNAL時(shí)胯舷,才能當(dāng)前節(jié)點(diǎn)才能被掛起刻蚯。
對(duì)線程的掛起及喚醒操作是通過(guò)使用UNSAFE類調(diào)用JNI方法實(shí)現(xiàn)的。當(dāng)然桑嘶,還提供了掛起指定時(shí)間后喚醒的API炊汹,在后面我們會(huì)講到。
到此為止逃顶,一個(gè)線程對(duì)于鎖的一次競(jìng)爭(zhēng)才告于段落讨便,結(jié)果有兩種,要么成功獲取到鎖(不用進(jìn)入到AQS隊(duì)列中)以政,要么霸褒,獲取失敗,被掛起盈蛮,等待下次喚醒后繼續(xù)循環(huán)嘗試獲取鎖废菱,值得注意的是,AQS的隊(duì)列為FIFO隊(duì)列抖誉,所以殊轴,每次被CPU假喚醒,且當(dāng)前線程不是出在頭節(jié)點(diǎn)的位置袒炉,也是會(huì)被掛起的旁理。AQS通過(guò)這樣的方式,實(shí)現(xiàn)了競(jìng)爭(zhēng)的排隊(duì)策略我磁。
看完了獲取鎖孽文,在看看釋放鎖,具體看代碼之前夺艰,我們可以先繼續(xù)猜下芋哭,釋放操作需要做哪些事情:
因?yàn)楂@取鎖的線程的節(jié)點(diǎn),此時(shí)在AQS的頭節(jié)點(diǎn)位置劲适,所以楷掉,可能需要將頭節(jié)點(diǎn)移除。
而應(yīng)該是直接釋放鎖,然后找到AQS的頭節(jié)點(diǎn)烹植,通知它可以來(lái)競(jìng)爭(zhēng)鎖了斑鸦。
是不是這樣呢?我們繼續(xù)來(lái)看下,同樣我們用ReentrantLock的FairSync來(lái)說(shuō)明:
unlock方法調(diào)用了AQS的release方法草雕,同樣傳入了參數(shù)1巷屿,和獲取鎖的相應(yīng)對(duì)應(yīng),獲取一個(gè)鎖墩虹,標(biāo)示為+1嘱巾,釋放一個(gè)鎖,標(biāo)志位-1诫钓。
同樣旬昭,release為空方法,子類自己實(shí)現(xiàn)邏輯:
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread()) //如果釋放的線程和獲取鎖的線程不是同一個(gè)菌湃,拋出非法監(jiān)視器狀 態(tài)異常问拘。
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {//因?yàn)槭侵厝氲年P(guān)系,不是每次釋放鎖c都等于0惧所,直到最后一次釋放鎖時(shí)骤坐,才通知AQS不需要再記錄哪個(gè)線程正在獲取鎖。
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
釋放鎖下愈,成功后纽绍,找到AQS的頭節(jié)點(diǎn),并喚醒它即可:
值得注意的是势似,尋找的順序是從隊(duì)列尾部開(kāi)始往前去找的最前面的一個(gè)waitStatus小于0的節(jié)點(diǎn)拌夏。
到此,ReentrantLock的lock和unlock方法已經(jīng)基本解析完畢了叫编,唯獨(dú)還剩下一個(gè)非公平鎖NonfairSync沒(méi)說(shuō)辖佣,其實(shí)霹抛,它和公平鎖的唯一區(qū)別就是獲取鎖的方式不同搓逾,一個(gè)是按前后順序一次獲取鎖,一個(gè)是搶占式的獲取鎖杯拐,那ReentrantLock是怎么實(shí)現(xiàn)的呢霞篡?再看兩段代碼:
非公平鎖的lock方法的處理方式是: 在lock的時(shí)候先直接cas修改一次state變量(嘗試獲取鎖),成功就返回端逼,不成功再排隊(duì)朗兵,從而達(dá)到不排隊(duì)直接搶占的目的。
而對(duì)于公平鎖:則是老老實(shí)實(shí)的開(kāi)始就走AQS的流程排隊(duì)獲取鎖顶滩。如果前面有人調(diào)用過(guò)其lock方法余掖,則排在隊(duì)列中前面,也就更有機(jī)會(huì)更早的獲取鎖礁鲁,從而達(dá)到“公平”的目的盐欺。
總結(jié)
這篇文章赁豆,我們從ReentrantLock出發(fā),完整的分析了AQS獨(dú)占功能的API及內(nèi)部實(shí)現(xiàn)冗美,總的來(lái)說(shuō)魔种,思路其實(shí)并不復(fù)雜,還是使用的標(biāo)志位+隊(duì)列的方式粉洼,記錄獲取鎖节预、競(jìng)爭(zhēng)鎖、釋放鎖等一系列鎖的狀態(tài)属韧,或許用更準(zhǔn)確一點(diǎn)的描述的話安拟,應(yīng)該是使用的標(biāo)志位+隊(duì)列的方式,記錄鎖宵喂、競(jìng)爭(zhēng)去扣、釋放等一系列獨(dú)占的狀態(tài),因?yàn)檎驹贏QS的層面state可以表示鎖樊破,也可以表示其他狀態(tài)愉棱,它并不關(guān)心它的子類把它變成一個(gè)什么工具類,而只是提供了一套維護(hù)一個(gè)獨(dú)占狀態(tài)哲戚。甚至奔滑,最準(zhǔn)確的是AQS只是維護(hù)了一個(gè)狀態(tài),因?yàn)樗成伲瑒e忘了朋其,它還有一套共享狀態(tài)的API,所以脆炎,AQS只是維護(hù)一個(gè)狀態(tài)梅猿,一個(gè)控制各個(gè)線程何時(shí)可以訪問(wèn)的狀態(tài),它只對(duì)狀態(tài)負(fù)責(zé)秒裕,而這個(gè)狀態(tài)表示什么含義袱蚓,由子類自己去定義。
極樂(lè)科技知乎專欄:深度解析Java 8:JDK1.8 AbstractQueuedSynchronizer的實(shí)現(xiàn)分析