深度解析Java 8:JDK1.8 AbstractQueuedSynchronizer的實現(xiàn)分析

更多多線程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é)點從隊列中移除。
        }
    }

這塊代碼有幾點需要說明:

  1. 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)分析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,923評論 6 535
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,740評論 3 420
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,856評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,175評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,931評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,321評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,383評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,533評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,082評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,891評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,618評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,319評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,732評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,987評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,794評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,076評論 2 375

推薦閱讀更多精彩內(nèi)容