轉載-深度解析Java 8:JDK1.8 AbstractQueuedSynchronizer的實現分析(上)

好文,寫的比我好。歡迎去出處鑑賞:node

http://www.infoq.com/cn/articles/jdk1.8-abstractqueuedsynchronizer安全

前言

Java中的FutureTask做爲可異步執行任務並可獲取執行結果而被你們所熟知。一般可使用future.get()來獲取線程的執行結果,在線程執行結束以前,get方法會一直阻塞狀態,直到call()返回,其優勢是使用線程異步執行任務的狀況下還能夠獲取到線程的執行結果,可是FutureTask的以上功能倒是依靠經過一個叫AbstractQueuedSynchronizer的類來實現,至少在JDK 1.五、JDK1.6版本是這樣的(從1.7開始FutureTask已經被其做者Doug Lea修改成再也不依賴AbstractQueuedSynchronizer實現了,這是JDK1.7的變化之一)。可是AbstractQueuedSynchronizer在JDK1.8中還有以下圖所示的衆多子類:架構

這些JDK中的工具類或多或少都被你們用過不止一次,好比ReentrantLock,咱們知道ReentrantLock的功能是實現代碼段的併發訪問控制,也就是一般意義上所說的鎖,在沒有看到AbstractQueuedSynchronizer前,可能會覺得它的實現是經過相似於synchronized,經過對對象加鎖來實現的。但事實上它僅僅是一個工具類!沒有使用更「高級」的機器指令,不是關鍵字,也不依靠JDK編譯時的特殊處理,僅僅做爲一個普普統統的類就完成了代碼塊的併發訪問控制,這就更讓人疑問它怎麼實現的代碼塊的併發訪問控制的了。那就讓咱們一塊兒來仔細看下Doug Lea怎麼去實現的這個鎖。爲了方便,本文中使用AQS代替AbstractQueuedSynchronizer。併發

細說AQS

相關廠商內容異步

關於紅包、SSD雲盤等核心技術集錦!

跟技術大牛,侃侃容器那些事兒!

中國技術開放日上海站:FinTech-技術重定金融,將來大有不一樣(免費報名)

極光數據服務,教你如何洞察企業數字DNA

58集團架構與大數據應用創新專場

在深刻分析AQS以前,我想先從AQS的功能上說明下AQS,站在使用者的角度,AQS的功能能夠分爲兩類:獨佔功能和共享功能,它的全部子類中,要麼實現並使用了它獨佔功能的API,要麼使用了共享鎖的功能,而不會同時使用兩套API,即使是它最有名的子類ReentrantReadWriteLock,也是經過兩個內部類:讀鎖和寫鎖,分別實現的兩套API來實現的,爲何這麼作,後面咱們再分析,到目前爲止,咱們只須要明白AQS在功能上有獨佔控制和共享控制兩種功能便可。工具

獨佔鎖

在真正對解讀AQS以前,我想先從使用了它獨佔控制功能的子類ReentrantLock提及,分析ReentrantLock的同時看一看AQS的實現,再推理出AQS獨特的設計思路和實現方式。最後,再看其共享控制功能的實現。大數據

對於ReentrantLock,使用過的同窗應該都知道,一般是這麼用它的:ui

reentrantLock.lock()
        //do something
        reentrantLock.unlock()

ReentrantLock會保證 do something在同一時間只有一個線程在執行這段代碼,或者說,同一時刻只有一個線程的lock方法會返回。其他線程會被掛起,直到獲取鎖。從這裏能夠看出,其實ReentrantLock實現的就是一個獨佔鎖的功能:有且只有一個線程獲取到鎖,其他線程所有掛起,直到該擁有鎖的線程釋放鎖,被掛起的線程被喚醒從新開始競爭鎖。沒錯,ReentrantLock使用的就是AQS的獨佔API實現的。spa

那如今咱們就從ReentrantLock的實現開始一塊兒看看重入鎖是怎麼實現的。線程

首先看lock方法:

如FutureTask(JDK1.6)同樣,ReentrantLock內部有代理類完成具體操做,ReentrantLock只是封裝了統一的一套API而已。值得注意的是,使用過ReentrantLock的同窗應該知道,ReentrantLock又分爲公平鎖和非公平鎖,因此,ReentrantLock內部只有兩個sync的實現:

公平鎖:每一個線程搶佔鎖的順序爲前後調用lock方法的順序依次獲取鎖,相似於排隊吃飯。

非公平鎖:每一個線程搶佔鎖的順序不定,誰運氣好,誰就獲取到鎖,和調用lock方法的前後順序無關,相似於堵車時,加塞的那些XXXX。

到這裏,經過ReentrantLock的功能和鎖的所謂排不排隊的方式,咱們是否能夠這麼猜想ReentrantLock或者AQS的實現(如今不清楚誰去實現這些功能):有那麼一個被volatile修飾的標誌位叫作key,用來表示有沒有線程拿走了鎖,或者說,鎖還存不存在,還須要一個線程安全的隊列,維護一堆被掛起的線程,以致於當鎖被歸還時,能通知到這些被掛起的線程,能夠來競爭獲取鎖了。

至於公平鎖和非公平鎖,惟一的區別是在獲取鎖的時候是直接去獲取鎖,仍是進入隊列排隊的問題了。爲了驗證咱們的猜測,咱們繼續看一下ReentrantLock中公平鎖的實現:

調用到了AQS的acquire方法:

從方法名字上看語義是,嘗試獲取鎖,獲取不到則建立一個waiter(當前線程)後放到隊列中,這和咱們猜想的好像很相似。[G1]

先看下tryAcquire方法:

留空了,Doug Lea是想留給子類去實現(既然要給子類實現,應該用抽象方法,可是Doug Lea沒有這麼作,緣由是AQS有兩種功能,面向兩種使用場景,須要給子類定義的方法都是抽象方法了,會致使子類不管如何都須要實現另一種場景的抽象方法,顯然,這對子類來講是不友好的。)

看下FairSync的tryAcquire方法:

getState方法是AQS的方法,由於在AQS裏面有個叫statede的標誌位 :

事實上,這個state就是前面咱們猜測的那個「key」!

回到tryAcquire方法:

protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();//獲取當前線程
            int c = getState();  //獲取父類AQS中的標誌位
            if (c == 0) {
                if (!hasQueuedPredecessors() && 
                    //若是隊列中沒有其餘線程  說明沒有線程正在佔有鎖!
                    compareAndSetState(0, acquires)) { 
                    //修改一下狀態位,注意:這裏的acquires是在lock的時候傳遞來的,從上面的圖中能夠知道,這個值是寫死的1
                    setExclusiveOwnerThread(current);
                    //若是經過CAS操做將狀態爲更新成功則表明當前線程獲取鎖,所以,將當前線程設置到AQS的一個變量中,說明這個線程拿走了鎖。
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
             //若是不爲0 意味着,鎖已經被拿走了,可是,由於ReentrantLock是重入鎖,
             //是能夠重複lock,unlock的,只要成對出現行。一次。這裏還要再判斷一次 獲取鎖的線程是否是當前請求鎖的線程。
                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方法。

若是沒有獲取到鎖,按照咱們的描述,應該將當前線程放到隊列中去,只不過,在放以前,須要作些包裝。

先看addWaiter方法:

用當前線程去構造一個Node對象,mode是一個表示Node類型的字段,僅僅表示這個節點是獨佔的,仍是共享的,或者說,AQS的這個隊列中,哪些節點是獨佔的,哪些是共享的。

這裏lock調用的是AQS獨佔的API,固然,能夠寫死是獨佔狀態的節點。

建立好節點後,將節點加入到隊列尾部,此處,在隊列不爲空的時候,先嚐試經過cas方式修改尾節點爲最新的節點,若是修改失敗,意味着有併發,這個時候纔會進入enq中死循環,「自旋」方式修改。

將線程的節點接入到隊裏中後,固然還須要作一件事:將當前線程掛起!這個事,由acquireQueued來作。

在解釋acquireQueued以前,咱們須要先看下AQS中隊列的內存結構,咱們知道,隊列由Node類型的節點組成,其中至少有兩個變量,一個封裝線程,一個封裝節點類型。

而實際上,它的內存結構是這樣的(第一次節點插入時,第一個節點是一個空節點,表明有一個線程已經獲取鎖,事實上,隊列的第一個節點就是表明持有鎖的節點):

黃色節點爲隊列默認的頭節點,每次有線程競爭失敗,進入隊列後其實都是插入到隊列的尾節點(tail後面)後面。這個從enq方法能夠看出來,上文中有提到enq方法爲將節點插入隊列的方法:

再回來看看

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)) {
             //若是當前的節點是head說明他是隊列中第一個「有效的」節點,所以嘗試獲取,上文中有提到這個類是交給子類去擴展的。
                    setHead(node);//成功後,將上圖中的黃色節點移除,Node1變成頭節點。
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) && 
                //不然,檢查前一個節點的狀態爲,看當前獲取鎖失敗的線程是否須要掛起。
                    parkAndCheckInterrupt()) 
               //若是須要,藉助JUC包下的LockSopport類的靜態方法Park掛起當前線程。知道被喚醒。
                    interrupted = true;
            }
        } finally {
            if (failed) //若是有異常
                cancelAcquire(node);// 取消請求,對應到隊列操做,就是將當前節點從隊列中移除。
        }
    }

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

1. Node節點中,除了存儲當前線程,節點類型,隊列中先後元素的變量,還有一個叫waitStatus的變量,改變量用於描述節點的狀態,爲何須要這個狀態呢?

緣由是:AQS的隊列中,在有併發時,確定會存取必定數量的節點,每一個節點[G4] 表明了一個線程的狀態,有的線程可能「等不及」獲取鎖了,須要放棄競爭,退出隊列,有的線程在等待一些條件知足,知足後才恢復執行(這裏的描述很像某個J.U.C包下的工具類,ReentrankLock的Condition,事實上,Condition一樣也是AQS的子類)等等,總之,各個線程有各個線程的狀態,但總須要一個變量來描述它,這個變量就叫waitStatus,它有四種狀態:

分別表示:

  1. 節點取消
  2. 節點等待觸發
  3. 節點等待條件
  4. 節點狀態須要向後傳播。

只有當前節點的前一個節點爲SIGNAL時,才能當前節點才能被掛起。

2.  對線程的掛起及喚醒操做是經過使用UNSAFE類調用JNI方法實現的。固然,還提供了掛起指定時間後喚醒的API,在後面咱們會講到。

到此爲止,一個線程對於鎖的一次競爭才告於段落,結果有兩種,要麼成功獲取到鎖(不用進入到AQS隊列中),要麼,獲取失敗,被掛起,等待下次喚醒後繼續循環嘗試獲取鎖,值得注意的是,AQS的隊列爲FIFO隊列,因此,每次被CPU假喚醒,且當前線程不是出在頭節點的位置,也是會被掛起的。AQS經過這樣的方式,實現了競爭的排隊策略。

看完了獲取鎖,在看看釋放鎖,具體看代碼以前,咱們能夠先繼續猜下,釋放操做須要作哪些事情:

  1. 由於獲取鎖的線程的節點,此時在AQS的頭節點位置,因此,可能須要將頭節點移除。
  2. 而應該是直接釋放鎖,而後找到AQS的頭節點,通知它能夠來競爭鎖了。

是否是這樣呢?咱們繼續來看下,一樣咱們用ReentrantLock的FairSync來講明:

unlock方法調用了AQS的release方法,一樣傳入了參數1,和獲取鎖的相應對應,獲取一個鎖,標示爲+1,釋放一個鎖,標誌位-1。

一樣,release爲空方法,子類本身實現邏輯:

protected final boolean tryRelease(int releases) {
            int c = getState() - releases; 
            if (Thread.currentThread() != getExclusiveOwnerThread()) //若是釋放的線程和獲取鎖的線程不是同一個,拋出非法監視器狀態異常。
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {//由於是重入的關係,不是每次釋放鎖c都等於0,直到最後一次釋放鎖時,才通知AQS不須要再記錄哪一個線程正在獲取鎖。
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

釋放鎖,成功後,找到AQS的頭節點,並喚醒它便可:

值得注意的是,尋找的順序是從隊列尾部開始往前去找的最前面的一個waitStatus小於0的節點。

到此,ReentrantLock的lock和unlock方法已經基本解析完畢了,惟獨還剩下一個非公平鎖NonfairSync沒說,其實,它和公平鎖的惟一區別就是獲取鎖的方式不一樣,一個是按先後順序一次獲取鎖,一個是搶佔式的獲取鎖,那ReentrantLock是怎麼實現的呢?再看兩段代碼:

非公平鎖的lock方法的處理方式是: 在lock的時候先直接cas修改一次state變量(嘗試獲取鎖),成功就返回,不成功再排隊,從而達到不排隊直接搶佔的目的。

而對於公平鎖:則是老老實實的開始就走AQS的流程排隊獲取鎖。若是前面有人調用過其lock方法,則排在隊列中前面,也就更有機會更早的獲取鎖,從而達到「公平」的目的。

總結

這篇文章,咱們從ReentrantLock出發,完整的分析了AQS獨佔功能的API及內部實現,總的來講,思路其實並不複雜,仍是使用的標誌位+隊列的方式,記錄獲取鎖、競爭鎖、釋放鎖等一系列鎖的狀態,或許用更準確一點的描述的話,應該是使用的標誌位+隊列的方式,記錄鎖、競爭、釋放等一系列獨佔的狀態,由於站在AQS的層面state能夠表示鎖,也能夠表示其餘狀態,它並不關心它的子類把它變成一個什麼工具類,而只是提供了一套維護一個獨佔狀態。甚至,最準確的是AQS只是維護了一個狀態,由於,別忘了,它還有一套共享狀態的API,因此,AQS只是維護一個狀態,一個控制各個線程什麼時候能夠訪問的狀態,它只對狀態負責,而這個狀態表示什麼含義,由子類本身去定義。

感謝郭蕾對本文的審校。

相關文章
相關標籤/搜索