AbstractQueuedSynchronizer實現源碼解析(一)

AbstractQueuedSynchronizer簡介

    AbstractQueuedSynchronizer(AQS)是實現依賴於FIFO等待隊列的阻塞鎖定或者相關同步器(ReentrantLock, Semaphore等)的一個框架,在java.util.concurrent併發庫中有着舉足輕重的作用。AQS把單個原子int值來表示狀態(對於long有專屬類AbstractQueuedLongSynchronizer),一般子類通過繼承該類後,定義哪種狀態屬於當前對象被獲取或者被釋放,一般使用getState()、setState(int)、compareAndSetState(int, int)方法來更新int值。

    AQS支持獨佔(exclusive)模式或者共享(shared)模式。在獨佔模式下,只有指定線程可以成功獲得鎖。在共享模式下,多個線程可以獲取鎖成功。

    AQS內部定義一個嵌套類ConditionObject,可以用做Condition實現。

    一般情況下,子類爲來實現對應的同步需求,可以重載以下方法(具體原理稍後解釋)
    tryAcquire(int)  試圖在獨佔模式下獲取對象狀態。此方法應該查詢是否允許它在獨佔模式下獲取對象狀態,如果允許,則獲取它。

    tryRelease(int)  試圖設置狀態來反映獨佔模式下的一個釋放。

    tryAcquireShared(int)  試圖在共享模式下獲取對象狀態。此方法應該查詢是否允許它在共享模式下獲取對象狀態,如果允許,則獲取它。  

    tryReleaseShared(int)  試圖設置狀態來反映共享模式下的一個釋放。

    isHeldExclusively()  如果對於當前(正調用的)線程,同步是以獨佔方式進行的,則返回 true。

具體實現解析

1. AQS等待隊列原理

    AQS內部以一個雙向鏈表隊列作爲線程等待隊列(CLH鎖隊列變種),每個等待這把鎖的線程作爲鏈表的一個結點。每個結點包含一個“status”域來跟蹤每條線程是否需要阻塞。當祖先被釋放的時候,這個結點會被觸發。在等待隊列中的處於第一個的線程可以嘗試獲取鎖,但處於第一不保證一定成功獲取,只代表它有權去競爭,所以當前釋放的競爭者線程可能需要重新等待。
    作爲一個FIFO等待隊列的實現,喚醒結點從head開始,插入結點從tail開始。

    //等待隊列結構如下:
           +------+  prev +-----+       +-----+
      head | dummy| <---- |     | <---- |     |  tail
           | node |  next |     |       |     |
           +------+ ----> +-----+ ----> +-----+

    head一直只會引用一個無效空結點,其後繼結點纔是第一個表示等待該鎖的線程結點。

    prev主要用在取消獲取鎖操作上。如果一個結點被取消來,他的後繼結點一般會被重新連接到一個非取消的祖先。

    next用來實現阻塞機制。祖先通過遍歷next連接觸發下一個結點去決定被喚醒的線程。後繼結點的查找必須避免與新進入隊列的結點設置next域競爭。如果出現後繼結點爲null的情況下,可以通過從tail開始從後往前的查詢真正的繼承者解決競爭。(或者,next連接可以看到避免經常從後往前的掃描的優化)。

    CLH隊列需要一個無效的頭結點作爲初始化。不過實現裏並沒有在構造時刻創建,因爲可能如果沒有產生競爭的時候,這個無效頭結點就浪費。因此是在第一次競爭的時候創建這個無效頭結點。

    結點狀態有以下幾種
        static final int CANCELLED =  1;
        static final int SIGNAL    = -1;        
        static final int CONDITION = -2;
        static final int PROPAGATE = -3;

        另外還包括默認值0。

    SIGNAL               該結點的後繼者已經(或者很快就會)被阻塞,所以當前結點必須在釋放或者取消的時候喚醒它的後繼結點。

    CANCELLED       當前結點由於超時或者interrupt被取消,一經設置,結點無法改變此狀態。換句話說,該線程不會再次被阻塞。

    CONDITION        結點目前在結點隊列裏。當重新進入等待隊列後,這個狀態會被設置爲0。

    PROGPAGATE   一個releaseShared應該被傳遞給其他結點。在doReleaseShared裏設置(只針對頭結點)去確保傳遞會繼續。

    這些狀態值裏,非負數表示一個結點不需要被觸發。所以,大部分代碼值需要簡單測試正負即可。

    在條件狀態下的線程使用相同結點表達,不過使用裏額外的連接。由於條件狀態只能在獨佔模式下訪問,因此只需要簡單的結點連接(不存在併發)。在等待時候,一個結點會插入到條件隊列,在觸發的時候,這個結點會把轉移到等待隊列,一個特別的狀態值(CONDITION)會被設置去標記這個結點在哪個隊列上。

2.源碼剖析

    AQS作爲java併發的重中之中,內部實現相當巧妙,但千里之行始於足下,我們先從獨佔模式開始,從acquire入手剖析如果多個線程同時爭搶鎖的時候,爭搶失敗的線程如何進入隊列等待鎖。

(1)acquire獲取鎖流程

    首先,獲取鎖一般是acquire函數,不過AQS提供了acquire的多個變種,其中包括(僅限獨佔模式)

//在獨佔模式下獲取鎖,忽略線程interrupted
public final void acquire(int arg)


//在獨佔模式下獲取鎖,如果線程interrupted,拋出異常,放棄等待鎖。
public final void acquireInterruptibly(int arg)


//嘗試在獨佔模式下獲取鎖,如果線程interrupted,拋出異常,同時等待超過指定時間時,會返回失敗
public final boolean tryAcquireNanos(int arg, long nanosTimeout)

    事實上,三種變種的具體實現結構差不多,因此我們先集中剖析acquire獲取。函數源碼如下。

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
    tryAcquire由子類重新實現邏輯,一般子類可以通過判斷當前是否已經有線程獲取了鎖,或者判斷當前線程是否已經獲得了鎖(可重入)等等,通過判斷當前線程能否獲得鎖,返回了true,證明當前線程獲取了鎖,也就不需要競爭,會直接退出函數,如果返回了false,則需要把當前線程加入等待隊列。
    加入等待隊列是通過addWaiter實現,函數源碼如下
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

    這裏,我們賦予參數mode爲Node.EXCLUSIVE,查看定義是一個null的Node,表達這是在獨佔模式。(在共享模式下,這是一個經過初始化的final空結點,用於區分獨佔結點)。把當前線程以及模式作爲參數構造一個新的Node之後,首先嚐試從tail結點插入,如果tail結點沒有初始化,或者產生了競爭的時候,就會調用enq函數利用循環CAS來保證從tail插入結點。

    enq函數實現如下

    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }
    //head,tail初始化後:
           +------+
      head | dummy| tail
           | node |
           +------+


    //添加第一個結點:
           +------+  prev +-----+
      head | dummy| <---- |     | tail
           | node |  next |     |
           +------+ ----> +-----+
    這是一個循環的CAS,循環內部會檢查tail,由於這裏是產生競爭纔會執行,所以tail爲null即head和tail還沒有進行初始化,於是把tail和head引用一個無效的空結點。當初始化完成後,就是典型的CAS把當前結點插入到tail,成功就會返回到上一步。注意,這時head引用的還是無效空結點,tail指向實際的結點。
    addWaiter把當前線程入隊後,就會到acquireQueued函數,代碼如下
    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)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

    首先該函數主體也是一個循環和條件判斷來避免競爭帶來的問題。在循環內部,首先獲取調用函數predecessor獲取祖先結點,函數實現裏有一個null判斷prev,會拋出NullPointerException(註釋作用爲help the VM,個人感覺方便從異常裏查找問題)。然後判斷p == head是由於頭結點的直接後繼纔是第一個能夠重新競爭的線程,如果是頭結點的直接後繼結點,這裏再次重新判斷tryAcquire來嘗試獲取鎖,如果成功的話,則會調用setHead把head指向當前結點,然後返回。如果tryAcquire失敗,則會通過shouldParkAfterFailedAcquire把當前線程的waitStatus的狀態標識設置爲SIGNAL,然後返回false,再重新嘗試獲取鎖,如果再次失敗,則會進入parkAndCheckInerrupt,把當前線程進入阻塞狀態,直到release把當前線程喚醒,然後再次循環進行tryAcquire獲取鎖,除非成功退出或者失敗再次進入阻塞,這樣做可以避免多線程競爭鎖帶來的問題。

    先看看shouldParkAfterFailedAcquire實現。

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
             return true;
        if (ws > 0) {
             do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
    傳入的參數是當前結點以及其前繼結點。由於默認的時候waitStatus爲0,因此第一次調用的時候會把waitStatus設置爲SIGNAL,如果waitStatus大於0(CANCELLED狀態),則會迭代查找前繼結點。返回true,則會在往後的操作裏進行阻塞狀態,如果返回false則會進入上一步的循環重新嘗試獲取鎖。因此這個函數主要在更改前繼結點狀態到SIGNAL。這樣做的目的,對比其更改到SIGNAL後直接返回true,是爲了避免多線程競爭的問題,這個競爭與下面釋放鎖有關,詳細例子見下面釋放鎖release的剖析。

    如果shouldParkAfterFailedAcquire返回true,則會執行parkAndCheckInterrupt,代碼如下

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

    LockSupport.park會把當前線程進行阻塞狀態。同時返回當前線程是否被interrupt。

    另外,我們看看如果發生了異常退出循環,需要取消正在獲取鎖的行爲時,調用了cancelAcquire。
     private void cancelAcquire(Node node) {
        if (node == null)
            return;


        node.thread = null;


        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;


        Node predNext = pred.next;


        node.waitStatus = Node.CANCELLED;
        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {


            int ws;
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                unparkSuccessor(node);
            }


            node.next = node; // help GC
        }
    }

    首先,如果node爲null則不做任何事情直接return。然後,通過while循環判斷,直接前繼結點的waitStatus > 0(也就是CANCELLED)來跳過已經被取消來的前繼結點,然後predNext顯然就是需要取消鏈接的結點(通過CAS替換next域),但由於多線程競爭存在,因此下面的CAS更改可能會失敗,也就是說在與另外的cancel或者signal操作中競爭失敗,因此可以不需要另外的操作也不會影響。給node.waitStatus = Node.CANCELLED可以不需要CAS,這樣其它結點在判斷中就會跳過該結點。

    接下來判斷,如果被取消的結點剛好是tail結點,並且CAS可以成功把tail結點替換成非取消的前繼結點pred,則同時利用CAS把pred的next域替換爲null。如果被取消的結點不是tail結點,則會再次判斷,如果該結點並非頭結點的直接後繼(因爲可能需要被喚醒)並且pred的後繼結點需要喚醒(也就waitStatus爲SIGNAL或者爲<=0),這樣就把被取消結點的next域賦給非取消前繼結點pred的next域。否則的話我們便喚醒被取消結點node的後繼結點,嘗試獲取鎖。

(2)release釋放鎖流程
    這樣大致瞭解來acquire獲取鎖的流程,我們來看看release釋放鎖究竟做了什麼。
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

    首先判斷tryRelease是否允許釋放鎖,一般子類重寫tryRelease會根據需要判斷能否釋放當前鎖中的等待隊列中的其他結點。當tryRelease返回true的時候,則會判斷head結點是否爲null並且head的waitStatus是否爲非0,如果判斷成功,則會調用unparkSuccessor來釋放後繼結點。

    注意,這裏release鎖便與上面的獲取鎖先更改SIGANL,然後再返回循環重新獲取鎖的做法有競爭問題,我們先假設shouldParkAfterFailedAcquire函數更改了SIGNAL狀態後直接返回true,把當前線程進入阻塞,即
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        ...
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);


        //更改了SIGNAL之後返回true,便把當前線程進入阻塞狀態
        return true;
        ...
    }
    那麼當我們在獲取鎖的時候競爭失敗,進入了shouldParkAfterFailedAcquire之後,此時,release函數被調用,判斷waitStatus由於仍然沒改變SIGNAL,h.waitStatus == 0,然後就會退出函數,但此時shouldParkAfterFailedAcquire仍然執行,把當前線程阻塞了,這樣,就會由於併發問題進入死鎖狀態。因此,shouldParkAfterFailedAcquire採取的做法是先將狀態更改爲SIGNAL,然後返回false,這樣返回上一個堆棧裏的循環,就會還有一次tryAcquire的機會,這時當前線程便可以成功獲取了鎖,避免了死鎖狀態。
    我們回到release函數,判斷成功後,執行unparkSuccessor,代碼如下。
    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

    函數首先判斷waitStatus,如果爲負,則變爲0,表明準備喚醒後繼結點,然後一般情況下後繼結點就是next域,但由於競爭存在,因此可能被cancelled或者爲null,因此需要從tail到head進行遍歷找出真正的沒有被cancelled的後繼結點。最後則調用LockSupport.unpark把線程喚醒,被喚醒的線程就會在acquireQueued的循環裏再次重新嘗試獲取鎖,這樣在循環裏重新獲取鎖的時候,由於有可能此時有外部線程調用acquire獲取鎖,這樣兩者競爭,如果失敗的話,則失敗的一方就會進入阻塞狀態。

    這樣一來,acquire獲取鎖和release釋放鎖的流程大致清楚了,接下來看看其他變種的實現。
(3)acquire的其它變種。
    首先看看acquireInterruptibly變種。
   public final void acquireInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (!tryAcquire(arg))
            doAcquireInterruptibly(arg);
    }


    private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    可以看出,在acquireInterruptibly的開始添加了Thread.interrupted判斷後,拋出異常,以及在parkAndCheckInterrupt裏返回true拋出異常外,大體與acquire沒有區別。

    我們再來看看tryAcquireNanos變種。

    public final boolean tryAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquire(arg) ||
            doAcquireNanos(arg, nanosTimeout);
    }


    private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
        long lastTime = System.nanoTime();
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
                if (nanosTimeout <= 0)
                    return false;
                if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                long now = System.nanoTime();
                nanosTimeout -= now - lastTime;
                lastTime = now;
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
    tryAcquireNanos裏同樣多了一個Thread.interrupted的判斷,而doAcquireNanos則稍微有點不一樣,除去一些超時的判斷之後,一個很關鍵的判斷nanosTimeout > spinForTimeoutThreshold,按照註釋,spinForTimeoutThreshold是一個大致的閥值,當小於這個閥值的時候,旋轉一次的時間比parkNanos會比更加快,此時選擇旋轉的話,會提高非常短時間的反應性,也就是說會更加精確達到超時控制。
    這樣,本次代碼剖析第一步就完結了,在這裏,我們先大致介紹了AQS的框架以及實現原理,然後從代碼上剖析了獲取鎖和釋放鎖的具體實現原理,從實現上可以看到對於多線程併發要注意的競爭問題。下一步將會進行關於共享模式的獲取鎖和釋放鎖的分析。
發佈了36 篇原創文章 · 獲贊 4 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章