更多多線程API解析請參考jdk8 版本并發(fā)源碼解讀
前言
Java中的FutureTask作為可異步執(zhí)行任務(wù)并可獲取執(zhí)行結(jié)果而被大家所熟知。通常可以使用future.get()來獲取線程的執(zhí)行結(jié)果,在線程執(zhí)行結(jié)束之前,get方法會一直阻塞狀態(tài),直到call()返回,其優(yōu)點是使用線程異步執(zhí)行任務(wù)的情況下還可以獲取到線程的執(zhí)行結(jié)果,但是FutureTask的以上功能卻是依靠通過一個叫AbstractQueuedSynchronizer的類來實現(xiàn),至少在JDK 1.5、JDK1.6版本是這樣的(從1.7開始FutureTask已經(jīng)被其作者Doug Lea修改為不再依賴AbstractQueuedSynchronizer實現(xiàn)了,這是JDK1.7的變化之一)。但是AbstractQueuedSynchronizer在JDK1.8中還有如下圖所示的眾多子類:
這些JDK中的工具類或多或少都被大家用過不止一次,比如ReentrantLock,我們知道ReentrantLock的功能是實現(xiàn)代碼段的并發(fā)訪問控制,也就是通常意義上所說的鎖,在沒有看到AbstractQueuedSynchronizer前,可能會以為它的實現(xiàn)是通過類似于synchronized,通過對對象加鎖來實現(xiàn)的。但事實上它僅僅是一個工具類!沒有使用更“高級”的機(jī)器指令,不是關(guān)鍵字,也不依靠JDK編譯時的特殊處理,僅僅作為一個普普通通的類就完成了代碼塊的并發(fā)訪問控制,這就更讓人疑問它怎么實現(xiàn)的代碼塊的并發(fā)訪問控制的了。那就讓我們一起來仔細(xì)看下Doug Lea怎么去實現(xiàn)的這個鎖。為了方便,本文中使用AQS代替AbstractQueuedSynchronizer。
細(xì)說AQS
在深入分析AQS之前,我想先從AQS的功能上說明下AQS,站在使用者的角度,AQS的功能可以分為兩類:獨(dú)占功能和共享功能,它的所有子類中,要么實現(xiàn)并使用了它獨(dú)占功能的API,要么使用了共享鎖的功能,而不會同時使用兩套API,即便是它最有名的子類ReentrantReadWriteLock,也是通過兩個內(nèi)部類:讀鎖和寫鎖,分別實現(xiàn)的兩套API來實現(xiàn)的,為什么這么做,后面我們再分析,到目前為止,我們只需要明白AQS在功能上有獨(dú)占控制和共享控制兩種功能即可。
獨(dú)占鎖
在真正對解讀AQS之前,我想先從使用了它獨(dú)占控制功能的子類ReentrantLock說起,分析ReentrantLock的同時看一看AQS的實現(xiàn),再推理出AQS獨(dú)特的設(shè)計思路和實現(xiàn)方式。最后,再看其共享控制功能的實現(xiàn)。
對于ReentrantLock,使用過的同學(xué)應(yīng)該都知道,通常是這么用它的:
reentrantLock.lock()
//do something
reentrantLock.unlock()
ReentrantLock會保證 do something在同一時間只有一個線程在執(zhí)行這段代碼,或者說,同一時刻只有一個線程的lock方法會返回。其余線程會被掛起,直到獲取鎖。從這里可以看出,其實ReentrantLock實現(xiàn)的就是一個獨(dú)占鎖的功能:有且只有一個線程獲取到鎖,其余線程全部掛起,直到該擁有鎖的線程釋放鎖,被掛起的線程被喚醒重新開始競爭鎖。沒錯,ReentrantLock使用的就是AQS的獨(dú)占API實現(xiàn)的。
那現(xiàn)在我們就從ReentrantLock的實現(xiàn)開始一起看看重入鎖是怎么實現(xiàn)的。
首先看lock方法:
如FutureTask(JDK1.6)一樣,ReentrantLock內(nèi)部有代理類完成具體操作,ReentrantLock只是封裝了統(tǒng)一的一套API而已。值得注意的是,使用過ReentrantLock的同學(xué)應(yīng)該知道,ReentrantLock又分為公平鎖和非公平鎖,所以,ReentrantLock內(nèi)部只有兩個sync的實現(xiàn):
公平鎖:每個線程搶占鎖的順序為先后調(diào)用lock方法的順序依次獲取鎖,類似于排隊吃飯。
非公平鎖:每個線程搶占鎖的順序不定,誰運(yùn)氣好,誰就獲取到鎖,和調(diào)用lock方法的先后順序無關(guān),類似于堵車時,加塞的那些XXXX。
到這里,通過ReentrantLock的功能和鎖的所謂排不排隊的方式,我們是否可以這么猜測ReentrantLock或者AQS的實現(xiàn)(現(xiàn)在不清楚誰去實現(xiàn)這些功能):有那么一個被volatile修飾的標(biāo)志位叫做key,用來表示有沒有線程拿走了鎖,或者說,鎖還存不存在,還需要一個線程安全的隊列,維護(hù)一堆被掛起的線程,以至于當(dāng)鎖被歸還時,能通知到這些被掛起的線程,可以來競爭獲取鎖了。
至于公平鎖和非公平鎖,唯一的區(qū)別是在獲取鎖的時候是直接去獲取鎖,還是進(jìn)入隊列排隊的問題了。為了驗證我們的猜想,我們繼續(xù)看一下ReentrantLock中公平鎖的實現(xiàn):
調(diào)用到了AQS的acquire方法:
從方法名字上看語義是,嘗試獲取鎖,獲取不到則創(chuàng)建一個waiter(當(dāng)前線程)后放到隊列中,這和我們猜測的好像很類似。
先看下tryAcquire方法:
留空了,Doug Lea是想留給子類去實現(xiàn)(既然要給子類實現(xiàn),應(yīng)該用抽象方法,但是Doug Lea沒有這么做,原因是AQS有兩種功能,面向兩種使用場景,需要給子類定義的方法都是抽象方法了,會導(dǎo)致子類無論如何都需要實現(xiàn)另外一種場景的抽象方法,顯然,這對子類來說是不友好的。)
看下FairSync的tryAcquire方法:
getState方法是AQS的方法,因為在AQS里面有個叫statede的標(biāo)志位 :
事實上,這個state就是前面我們猜想的那個“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() &&
//如果隊列中沒有其他線程 說明沒有線程正在占有鎖!
compareAndSetState(0, acquires)) {
//修改一下狀態(tài)位,注意:這里的acquires是在lock的時候傳遞來的,從上面的圖中可以知道,這個值是寫死的1
setExclusiveOwnerThread(current);
//如果通過CAS操作將狀態(tài)為更新成功則代表當(dāng)前線程獲取鎖,因此,將當(dāng)前線程設(shè)置到AQS的一個變量中,說明這個線程拿走了鎖。
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
//如果不為0 意味著,鎖已經(jīng)被拿走了,但是,因為ReentrantLock是重入鎖,
//是可以重復(fù)lock,unlock的,只要成對出現(xiàn)行。一次。這里還要再判斷一次 獲取鎖的線程是不是當(dā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方法。
如果沒有獲取到鎖,按照我們的描述,應(yīng)該將當(dāng)前線程放到隊列中去,只不過,在放之前,需要做些包裝。
先看addWaiter方法:
用當(dāng)前線程去構(gòu)造一個Node對象,mode是一個表示Node類型的字段,僅僅表示這個節(jié)點是獨(dú)占的,還是共享的,或者說,AQS的這個隊列中,哪些節(jié)點是獨(dú)占的,哪些是共享的。
這里lock調(diào)用的是AQS獨(dú)占的API,當(dāng)然,可以寫死是獨(dú)占狀態(tài)的節(jié)點。
創(chuàng)建好節(jié)點后,將節(jié)點加入到隊列尾部,此處,在隊列不為空的時候,先嘗試通過cas方式修改尾節(jié)點為最新的節(jié)點,如果修改失敗,意味著有并發(fā),這個時候才會進(jìn)入enq中死循環(huán),“自旋”方式修改。
將線程的節(jié)點接入到隊里中后,當(dāng)然還需要做一件事:將當(dāng)前線程掛起!這個事,由acquireQueued來做。
在解釋acquireQueued之前,我們需要先看下AQS中隊列的內(nèi)存結(jié)構(gòu),我們知道,隊列由Node類型的節(jié)點組成,其中至少有兩個變量,一個封裝線程,一個封裝節(jié)點類型。
而實際上,它的內(nèi)存結(jié)構(gòu)是這樣的(第一次節(jié)點插入時,第一個節(jié)點是一個空節(jié)點,代表有一個線程已經(jīng)獲取鎖,事實上,隊列的第一個節(jié)點就是代表持有鎖的節(jié)點):
黃色節(jié)點為隊列默認(rèn)的頭節(jié)點,每次有線程競爭失敗,進(jìn)入隊列后其實都是插入到隊列的尾節(jié)點(tail后面)后面。這個從enq方法可以看出來,上文中有提到enq方法為將節(jié)點插入隊列的方法:
再回來看看
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é)點是head說明他是隊列中第一個“有效的”節(jié)點,因此嘗試獲取,上文中有提到這個類是交給子類去擴(kuò)展的。
setHead(node);//成功后,將上圖中的黃色節(jié)點移除,Node1變成頭節(jié)點。
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
//否則,檢查前一個節(jié)點的狀態(tài)為,看當(dāng)前獲取鎖失敗的線程是否需要掛起。
parkAndCheckInterrupt())
//如果需要,借助JUC包下的LockSopport類的靜態(tài)方法Park掛起當(dāng)前線程。知道被喚醒。
interrupted = true;
}
} finally {
if (failed) //如果有異常
cancelAcquire(node);// 取消請求,對應(yīng)到隊列操作,就是將當(dāng)前節(jié)點從隊列中移除。
}
}
這塊代碼有幾點需要說明:
- Node節(jié)點中,除了存儲當(dāng)前線程,節(jié)點類型,隊列中前后元素的變量,還有一個叫waitStatus的變量,改變量用于描述節(jié)點的狀態(tài),為什么需要這個狀態(tài)呢?
原因是:AQS的隊列中,在有并發(fā)時,肯定會存取一定數(shù)量的節(jié)點,每個節(jié)點代表了一個線程的狀態(tài),有的線程可能“等不及”獲取鎖了,需要放棄競爭,退出隊列,有的線程在等待一些條件滿足,滿足后才恢復(fù)執(zhí)行(這里的描述很像某個J.U.C包下的工具類,ReentrankLock的Condition,事實上,Condition同樣也是AQS的子類)等等,總之,各個線程有各個線程的狀態(tài),但總需要一個變量來描述它,這個變量就叫waitStatus,它有四種狀態(tài):
分別表示:
節(jié)點取消
節(jié)點等待觸發(fā)
節(jié)點等待條件
節(jié)點狀態(tài)需要向后傳播。
只有當(dāng)前節(jié)點的前一個節(jié)點為SIGNAL時,才能當(dāng)前節(jié)點才能被掛起。
對線程的掛起及喚醒操作是通過使用UNSAFE類調(diào)用JNI方法實現(xiàn)的。當(dāng)然,還提供了掛起指定時間后喚醒的API,在后面我們會講到。
到此為止,一個線程對于鎖的一次競爭才告于段落,結(jié)果有兩種,要么成功獲取到鎖(不用進(jìn)入到AQS隊列中),要么,獲取失敗,被掛起,等待下次喚醒后繼續(xù)循環(huán)嘗試獲取鎖,值得注意的是,AQS的隊列為FIFO隊列,所以,每次被CPU假喚醒,且當(dāng)前線程不是出在頭節(jié)點的位置,也是會被掛起的。AQS通過這樣的方式,實現(xiàn)了競爭的排隊策略。
看完了獲取鎖,在看看釋放鎖,具體看代碼之前,我們可以先繼續(xù)猜下,釋放操作需要做哪些事情:
因為獲取鎖的線程的節(jié)點,此時在AQS的頭節(jié)點位置,所以,可能需要將頭節(jié)點移除。
而應(yīng)該是直接釋放鎖,然后找到AQS的頭節(jié)點,通知它可以來競爭鎖了。
是不是這樣呢?我們繼續(xù)來看下,同樣我們用ReentrantLock的FairSync來說明:
unlock方法調(diào)用了AQS的release方法,同樣傳入了參數(shù)1,和獲取鎖的相應(yīng)對應(yīng),獲取一個鎖,標(biāo)示為+1,釋放一個鎖,標(biāo)志位-1。
同樣,release為空方法,子類自己實現(xiàn)邏輯:
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread()) //如果釋放的線程和獲取鎖的線程不是同一個,拋出非法監(jiān)視器狀 態(tài)異常。
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {//因為是重入的關(guān)系,不是每次釋放鎖c都等于0,直到最后一次釋放鎖時,才通知AQS不需要再記錄哪個線程正在獲取鎖。
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
釋放鎖,成功后,找到AQS的頭節(jié)點,并喚醒它即可:
值得注意的是,尋找的順序是從隊列尾部開始往前去找的最前面的一個waitStatus小于0的節(jié)點。
到此,ReentrantLock的lock和unlock方法已經(jīng)基本解析完畢了,唯獨(dú)還剩下一個非公平鎖NonfairSync沒說,其實,它和公平鎖的唯一區(qū)別就是獲取鎖的方式不同,一個是按前后順序一次獲取鎖,一個是搶占式的獲取鎖,那ReentrantLock是怎么實現(xiàn)的呢?再看兩段代碼:
非公平鎖的lock方法的處理方式是: 在lock的時候先直接cas修改一次state變量(嘗試獲取鎖),成功就返回,不成功再排隊,從而達(dá)到不排隊直接搶占的目的。
而對于公平鎖:則是老老實實的開始就走AQS的流程排隊獲取鎖。如果前面有人調(diào)用過其lock方法,則排在隊列中前面,也就更有機(jī)會更早的獲取鎖,從而達(dá)到“公平”的目的。
總結(jié)
這篇文章,我們從ReentrantLock出發(fā),完整的分析了AQS獨(dú)占功能的API及內(nèi)部實現(xiàn),總的來說,思路其實并不復(fù)雜,還是使用的標(biāo)志位+隊列的方式,記錄獲取鎖、競爭鎖、釋放鎖等一系列鎖的狀態(tài),或許用更準(zhǔn)確一點的描述的話,應(yīng)該是使用的標(biāo)志位+隊列的方式,記錄鎖、競爭、釋放等一系列獨(dú)占的狀態(tài),因為站在AQS的層面state可以表示鎖,也可以表示其他狀態(tài),它并不關(guān)心它的子類把它變成一個什么工具類,而只是提供了一套維護(hù)一個獨(dú)占狀態(tài)。甚至,最準(zhǔn)確的是AQS只是維護(hù)了一個狀態(tài),因為,別忘了,它還有一套共享狀態(tài)的API,所以,AQS只是維護(hù)一個狀態(tài),一個控制各個線程何時可以訪問的狀態(tài),它只對狀態(tài)負(fù)責(zé),而這個狀態(tài)表示什么含義,由子類自己去定義。
極樂科技知乎專欄:深度解析Java 8:JDK1.8 AbstractQueuedSynchronizer的實現(xiàn)分析