CountDownLatch 源碼淺析



CountDownLatch 介紹

CountDownLatch是一個同步協助類,允許一個或多個線程等待,直到其他線程完成操作集。

CountDownLatch使用給定的計數值(count)初始化。await方法會阻塞直到當前的計數值(count)由于countDown方法的調用達到0,在這之后(即,count為0之后)所有等待的線程都會被釋放,并且隨后對await方法的調用都會立即返回。這是一個一次性現象 ———— count不會被重置。如果你需要一個重置count的版本,那么請考慮使用CyclicBarrier。

CountDownLatch是一個通用的同步工具,它能用于許多用途。一個使用’1’計數值初始化的CountDownLatch服務作為一個簡單的開關門:所有執行await的線程等待在門口,直到某個執行countDown方法的線程將門打開。一個使用‘N(count)’初始化的CountDownLatch能被用于使一個線程等待,直到N個線程完成某些動作,或者某些動作已經完成N次。

CountDownLatch一個很有用的性質是,它不要求你在可以繼續進行之前調用countDown方法等待count到達0,它只是簡單的防止任何線程超過await方法直到所有的線程都可以通過。
也就是說,你可以在任意時刻調用await,如果當前的count值非0,那么線程會等待直到count為0時才會繼續往下執行,否則如果count值為0,await方法會立即返回,你可以不被阻塞的繼續往下執行。

內存一致性作用:直到count到達0,一個線程調用countDown()方法之前的動作 happen-before 從另一個線程相應的await()方法返回之后的動作。
比如,threadB.await()、threadA.countDown(),那么threadA執行countDown()之前的動作,對于threadB的await()方法之后的動作都可見(當count為0時,threadB會從await()方法的阻塞中結束而繼續往下執行)。

AbstractQueuedSynchronizer

因為CountDownLatch是使用AbstractQueuedSynchronizer(AQS)的state來實現其同步控制的。CountDownLatch使用的是共享鎖模式,由于AQS除了共享鎖模式還有排他鎖模式,本文僅對CountDownLatch涉及到的共享鎖模式部分的內容進行介紹,關于排他鎖模式的部分會在 ReentrantLock 源碼淺析一文中介紹。

AQS提供一個框架用于實現依賴于先進先出(FIFO)等待隊列的阻塞鎖和同步器(信號量,事件等)。這個類被設計與作為一個有用的基類,一個依賴單一原子值為代表狀態的多種同步器的基類。子類必須將修改這個狀態值的方法定義為受保護的方法,并且該方法會根據對象(即,AbstractQueuedSynchronizer子類)被獲取和釋放的方式來定義這個狀態。根據這些,這個類的其他方法實現所有排隊和阻塞的機制。子類能夠維護其他的狀態屬性,但是只有使用『getState』方法、『setState』方法以及『compareAndSetState』方法來原子性的修改 int 狀態值的操作才能遵循相關同步性。

等待隊列節點類 ——— Node

等待隊列是一個CLH鎖隊列的變體。CLH通常被用于自旋鎖(CLH鎖是一種基于鏈表的可擴展、高性能、公平的自旋鎖,申請線程只在本地變量上自旋,它不斷輪詢前驅的狀態,如果發現前驅釋放了鎖就結束自旋。)。我們用它來代替阻塞同步器,但是使用相同的基本策略,該策略是持有一些關于一個線程在它前驅節點的控制信息。一個“status”字段在每個節點中用于保持追蹤是否一個線程需要被阻塞。一個節點會得到通知當它的前驅節點被釋放時。隊列中的每一個節點都作為一個持有單一等待線程的特定通知風格的監視器。狀態字段不會控制線程是否被授予鎖等。一個線程可能嘗試去獲取鎖如果它在隊列的第一個。但是首先這并不保證成功,它只是給與了競爭的權力(也就是說,隊列中第一個線程嘗試獲取鎖時,并不保證一定能得到鎖,它只是有競爭鎖的權力而已)。所以當前被釋放的競爭者線程可能需要重新等待獲取鎖。
(這里說的"隊列中的第一個的線程"指的時,從隊列頭開始往下的節點中,第一個node.thread != null的線程。因為,AQS隊列的head節點是一個虛節點,不是有個有效的等待節點,因此head節點的thread是為null的。)

為了排隊進入一個CLH鎖,你可以原子性的拼接節點到隊列中作為一個新的隊尾;對于出隊,你只要設置頭字段。(即,入隊操作時新的節點會排在CLH鎖隊列的隊尾,而出隊操作就是將待出隊的node設置為head。由此可見,在AQS中維護的這個等待隊列,head是一個無效的節點。初始化時head是一個new Node()節點;在后期的操作中,需要出隊的節點就會設置到head中。)

          +------+  prev +-----+       +-----+
     head |      | <---- |     | <---- |     |  tail
          +------+       +-----+       +-----+

插入到一個CLH隊列的請求只是一個對“tail”的單個原子操作,所以有一個簡單的從未入隊到入隊的原子分割點。類似的,出隊調用只需要修改“head”。然而,節點需要更多的工作來確定他們的后繼者是誰,部分是為了處理由于超時和中斷而導致的可能的取消。
(也就是說,一個node的后繼節點不一定就是node.next,因為隊列中的節點可能因為超時或中斷而取消了,而這些取消的節點此時還沒被移除隊列(也許正在移除隊列的過程中),而一個node的后繼節點指的是一個未被取消的有效節點,因此在下面的操作中你就會發現,在尋找后繼節點時,尋找的都是當前節點后面第一個有效節點,即非取消節點。)

“prev”(前驅)連接(原始的CLH鎖是不使用前驅連接的),主要用于處理取消。如果一個節點被取消了,它的后驅(通常)會重連接到一個未被取消的前驅。

另外我們使用“next”連接去實現阻塞機制。每個節點的線程ID被它們自己的節點所持有,所以前驅節點通知下一個節點可以被喚醒,這是通過遍歷下一個鏈接(即,next字段)來確定需要喚醒的線程。后繼節點的決定必須同‘新入隊的節點在設置它的前驅節點的“next”屬性操作(即,新入隊節點為newNode,在newNode的前驅節點preNewNode進行preNewNode.next = newNode操作)’產生競爭。一個解決方法是必要的話當一個節點的后繼看起來是空的時候,從原子更新“tail”向前檢測。(或者換句話說,next鏈接是一個優化,所以我們通常不需要反向掃描。)

取消引入了對基本算法的一些保守性。當我們必須為其他節點的取消輪詢時,我們不需要留意一個取消的節點是在我們節點的前面還是后面。它的處理方式是總是根據取消的節點喚醒其后繼節點,允許它們去連接到一個新的前驅節點,除非我們能夠標識一個未被取消的前驅節點來完成這個責任。

  • waitStatus
volatile int waitStatus;

狀態屬性,只有如下值:
① SIGNAL:
static final int SIGNAL = -1;
這個節點的后繼(或者即將被阻塞)被阻塞(通過park阻塞)了,所以當前節點需要喚醒它的后繼當它被釋放或者取消時。為了避免競爭,獲取方法必須首先表示他們需要一個通知信號,然后再原子性的嘗試獲取鎖,如果失敗,則阻塞。
也就是說,在獲取鎖的操作中,需要確保當前node的preNode的waitStatus狀態值為’SIGNAL’,才可以被阻塞,當獲取鎖失敗時。(『shouldParkAfterFailedAcquire』方法的用意就是這)
② CANCELLED:
static final int CANCELLED = 1;
這個節點由于超時或中斷被取消了。節點不會離開(改變)這個狀態。尤其,一個被取消的線程不再會被阻塞了。
③ CONDITION:
static final int CONDITION = -2;
這個節點當前在一個條件隊列中。它將不會被用于當做一個同步隊列的節點直到它被轉移到同步隊列中,轉移的同時狀態值(waitStatus)將會被設置為0。(這里使用這個值將不會做任何事情與該字段其他值對比,只是為了簡化機制)。
④ PROPAGATE:
static final int PROPAGATE = -3;
一個releaseShared操作必須被廣播給其他節點。(只有頭節點的)該值會在doReleaseShared方法中被設置去確保持續的廣播,即便其他操作的介入。
⑤ 0:不是上面的值的情況。
這個值使用數值排列以簡化使用。非負的值表示該節點不需要信號(通知)。因此,大部分代碼不需要去檢查這個特殊的值,只是為了標識。
對于常規的節點該字段會被初始化為0,競爭節點該值為CONDITION。這個值使用CAS修改(或者可能的話,無競爭的volatile寫)。

  • prev
volatile Node prev

連接到前驅節點,當前節點/線程依賴與這個節點waitStatus的檢測。分配發生在入隊時,并在出隊時清空(為了GC)。并且,一個前驅的取消,我們將短路當發現一個未被取消的節點時,未被取消的節點總是存在因為頭節點不能被取消:只有在獲取鎖操作成功的情況下一個節點才會成為頭節點。一個被取消的線程絕不會獲取成功,一個線程只能被它自己取消,不能被其他線程取消。

  • next
volatile Node next

連接到后繼的節點,該節點是當前的節點/線程釋放喚醒的節點。分配發生在入隊時,在繞過取消的前驅節點時進行調整,并在出隊列時清空(為了GC的緣故)。一個入隊操作(enq)不會被分配到前驅節點的next字段,直到tail成功指向當前節點之后(通過CAS來將tail指向當前節點。『enq』方法實現中,會先將node.prev = oldTailNode;在需要在CAS成功之后,即tail = node之后,再將oldTailNode.next = node;),所以當看到next字段為null時并不意味著當前節點是隊列的尾部了。無論如何,如果一個next字段顯示為null,我們能夠從隊列尾向前掃描進行復核。被取消的節點的next字段會被設置為它自己,而不是一個null,這使得isOnSyncQueue方法更簡單。

  • thread
volatile Thread thread

這個節點的入隊線程。在構建時初始化,在使用完后清除。

  • nextWaiter
Node nextWaiter

鏈接下一個等待條件的節點,或者一個指定的SHARED值。因為只有持有排他鎖時能訪問條件隊列,所以我們只需要一個簡單的單鏈表來維持正在等待條件的節點。它們接下來會被轉換到隊列中以去重新獲取鎖。因為只有排他鎖才有conditions,所以我們使用給一個特殊值保存的字段來表示共享模式。
也就是說,nextWaiter用于在排他鎖模式下表示正在等待條件的下一個節點,因為只有排他鎖模式有conditions;所以在共享鎖模式下,我們使用’SHARED’這個特殊值來表示該字段。

源碼分析

初始化
CountDownLatch doneSignal = new CountDownLatch(N);

CountDownLatch 使用了共享鎖模式。CountDownLatch 使用了一個內部類 Sync來實現CountDownLatch的同步控制,而Sync是AQS的一個實現類,它使用AQS的狀態(state)來表示count。

public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}

構造一個CountDownLatch使用給定的count值進行初始化。
count值最終是設置到sync(AbstractQueuedSynchronizer)里的state字段。

阻塞的流程分析

『await()』

public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

導致當前的線程等待直到latch被倒數到0,或者線程被中斷了。
如果當前的count是0,那么方法會立即返回,并且返回值為true。
如果當前的count大于0,則當前線程因為線程調度而變得不可用,并且處于休眠狀態,直到發生下面二件事之一:
① 由于countDown方法的調用當前的count達到0;
如果count達到0,那么這個方法將返回true。
② 其他線程中斷了當前的線程;
如果當前線程在進入這個方法時設置了中斷狀態;或者當前線程在等待時被設置了中斷狀態,那么“InterruptedException”異常將會拋出,并且當前的線程的中斷狀態會被清除。


『acquireSharedInterruptibly』

    public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }

獲取一個共享模式鎖,如果發生中斷則異常終止。實現首先會檢查中斷的狀態,然后執行至少一次的tryAcquireShared,成功的話返回。否則,線程將會入隊,可能會重復的阻塞和解阻塞,執行tryAcquireShared直到成功或者線程被中斷。
① 首先判斷當前的線程是否被標志為了中斷,如果被標志位了中斷,則拋出“InterruptedException”異常,并清除中斷標志;否則到第②步;
② 執行『tryAcquireShared』來嘗試獲取鎖,如果成功(即,返回>=0)。則返回true退出方法;否則到第③步
③ 執行doAcquireSharedInterruptibly。


『doAcquireSharedInterruptibly』

① 創建一個共享模式的節點,并將這個節點加入到等待隊列中。
② 獲取新創建好節點的前驅節點。如果前驅節點是head節點,則說明當前節點是隊列中第一個等待獲取鎖的節點,那么就執行『tryAcquireShared』方法嘗試獲取共享鎖。tryAcquireShared是由CountDownLatch重寫的方法。具體實現下面會詳細說明。這里先給出結果就是tryAcquireShared方法的返回值會小于0.也就說獲取共享鎖失敗。進入步驟③
③ 如果前驅節點不是head節點,或者當前節點獲取共享鎖失敗(即,步驟②)。那么執行『shouldParkAfterFailedAcquire』方法,該方法返回true則說明本次獲取共享鎖失敗需要阻塞(掛起)當前線程。接著執行『parkAndCheckInterrupt』方法,該方法會將當前線程掛起,直到被喚醒。
這就是阻塞情況下的一個主流程,可以知道的是,在這個邏輯過程中使用了大量的CAS來進行原子性的修改,當修改失敗的時候,是會通過for(;;)來重新循環的,也就是說『doAcquireSharedInterruptibly』使用自旋鎖(自旋+CAS)來保證在多線程并發的情況下,隊列節點狀態也是正確的以及在等待隊列的正確性,最終使得當前節點要么獲取共享鎖成功,要么被掛起等待喚醒。


下面我們來對阻塞情況下,涉及的方法進行進一步的展開。
『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;
}

根據給定的模式創建當前線程的節點,并將創建好的節點入隊(加入等待隊列尾部)。
首先在隊列非空的情況下會嘗試一次快速入隊,也就是通過嘗試一次CAS操作入隊,如果CAS操作失敗,則調用enq方法進行“自旋+CAS”方法將創建好的節點加入隊列尾。
在共享模式下,Node的mode(即,waitStatus)為’SHARED’。waitStatus是用于在排他鎖模式下當節點處于條件隊列時表示下一個等待條件的節點,所以在共享鎖模式下,我們使用’SHARED’這個特殊值來表示該字段。


『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;
            }
        }
    }
}

使用自旋鎖的方式(自旋+CAS)插入節點到等待隊列,如果等待隊列為空則初始化隊列。
初始化隊列:創建一個空節點(即,new Node()),將head和tail都指向這個節點。
然后才是將我們待插入的節點插入,即:emptyNode -> newNode. head指向emptyNode,tail指向newNode。


『tryAcquireShared』
在共享模式下嘗試獲取。這個方法需要查詢是否對象的狀態允許在共享模式下被獲取,如果允許則去獲取它。
這個方法總是被線程執行獲取共享鎖時被調用。如果這個方法報告失敗,那么獲取方法可能會使線程排隊等待,如果它(即,線程)還沒入隊的話,直到其他的線程發出釋放的信號。
默認實現拋出一個“UnsupportedOperationException”
返回:
a)< 0 : 一個負數的返回表示失敗;
b) 0 : 0表示在共享模式下獲取鎖成功,但是后續的獲取共享鎖將不會成功
c)> 0 : 大于0表示共享模式下獲取鎖成功,并且后續的獲取共享鎖可能也會成功,在這種情況下后續等待的線程必須檢查是否有效。

CountDownLatch對該方法進行了重寫:

protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}

如果當前的狀態值為0(即,count為0),則表示獲取成功(返回’1’);否則表示獲取失敗(返回’-1’)


『shouldParkAfterFailedAcquire』

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

檢查并修改一個節點的狀態,當該節點獲取鎖失敗時。返回true如果線程需要阻塞。這是主要的信號通知控制在所有的獲取鎖循環中。要求’pred’ == ‘node.prev’
① 如果pred.waitStatus == Node.SIGNAL。則說明node的前驅節點已經被要求去通知釋放它的后繼節點,所以node可以安全的被掛起(park)。然后,退出方法,返回true。
② 如果pred.waitStatus > 0。則說明node的前驅節點被取消了。那么跳過這個前驅節點并重新標志一個有效的前驅節點(即,waitStatus <= 0 的節點可作為有效的前驅節點),然后,退出方法,返回false。
③ 其他情況下,即pred.waitStatus為’0’或’PROPAGATE’。表示我們需要一個通知信號(即,當前的node需要喚醒的通知),但是當前還不能掛起node。調用『compareAndSetWaitStatus(pred, ws, Node.SIGNAL)』方法通過CAS的方式來修改前驅節點的waitStatus為“SIGNAL”。退出方法,返回false。
我們需要一個通知信號,主要是因為當前線程要被掛起了(park)。而如果waitStatus已經是’SIGNAL’的話就無需修改,直接掛起就好,而如果waitStatus是’CANCELLED’的話,說明prev已經被取消了,是個無效節點了,那么無需修改這個無效節點的waitStatus,而是需要先找到一個有效的prev。因此,剩下的情況就只有當waitStatus為’0’和’PROPAGAET’了(注意,waitStatus為’CONDITION’是節點不在等待隊列中,所以當下情況waitStatus不可能為’CONDITION’),這是我們需要將prev的waitStatus使用CAS的方式修改為’SIGNAL’,而且只有修改成功的情況下,當前的線程才能安全被掛起。
還值得注意的時,因此該方法的CAS操作都是沒有自旋的,所以當它操作完CAS后都會返回false,在外層的方法中會使用自旋,當發現返回的是false時,會再次調用該方法,以檢查保證有當前node有一個有效的prev,并且其waitStatus為’SIGNAL’,在此情況下當前的線程才會被掛起(park)。

釋放的流程分析

『countDown』

public void countDown() {
    sync.releaseShared(1);
}

減小latch的count,如果count達到0則釋放所有正在等待的線程。
如果當前的count大于0,那么減少count。如果減少后的count值為0,那么所有正在等待的線程因為線程調度的原因被重新啟用。
如果當前的count值已經是0了,那么什么都不會發生。


『releaseShared』

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

共享模式下的釋放。如果『tryReleaseShared』返回true的話,會使一個或多個線程重新啟動。


『tryReleaseShared』
在共享模式下,嘗試去設置狀態來反映一個釋放。
這個方法總是在線程執行釋放時被調用。
默認實現拋出一個UnsupportedOperationException異常。
返回:如果當前共享模式可能允許一個正在等待的獲取成功(正在等待的獲取可能是共享模式的,也可能是排他模式的),則返回true;否則,返回false。

CountDownLatch對該方法進行了重寫:

protected boolean tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    for (;;) {
        int c = getState();
        if (c == 0)
            return false;
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

減少count的值,如果count為0則發出釋放信號。
這里使用了"自旋+CAS”的方式來原子性的將state的值減少1,如果在此過程中state已經為0了(在并發情況下,可能已經被其他線程修改為了0),則返回false。否則根據修改后state的值是否等于0來返回boolean值。


『doReleaseShared』

private void doReleaseShared() {
    /*
     * Ensure that a release propagates, even if there are other
     * in-progress acquires/releases.  This proceeds in the usual
     * way of trying to unparkSuccessor of head if it needs
     * signal. But if it does not, status is set to PROPAGATE to
     * ensure that upon release, propagation continues.
     * Additionally, we must loop in case a new node is added
     * while we are doing this. Also, unlike other uses of
     * unparkSuccessor, we need to know if CAS to reset status
     * fails, if so rechecking.
     */
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

共享模式下的釋放動作 ———— 用信號通知后繼節點并且確保廣播。(注意:在排他鎖模式下,釋放只是相當于調用head的unparkSuccessor方法如果它需要通知喚醒的話。)
確保一個釋放的廣播,即使有其他線程正在進行獲取/釋放鎖。這個過程通常的方式是嘗試head的unparkSuccessor操作如果需要通知釋放的話。如果沒這么做,狀態會被設置為‘PROPAGATE’以確保在釋放,廣播繼續。此外,當我們正在做這個操作的時候如果新的節點被添加的話,我們需要重新循環再進行一次該操作。另外,不同于unparkSuccessor的其他用途,我們需要知道CAS重置狀態是否失敗,如果失敗則重新檢查。

在隊列非空的時候,該方法會釋放head的后繼節點,如果該節點可以被釋放的話。『(h != null && h != tail)』表示隊列非空,即有等待獲取鎖的節點;『(h == head)』表示,已經操作完釋放后繼節點,或者隊列已經空了(即,『(h == null || h == tail)』),那么就退出循環。否則如果循環過程中(即,『h != head』),頭結點發生了變化,則重新循環。
如果『if (h != null && h != tail)』為true,那么:
① 如果head的waitStatus為’SIGNAL’,則說明head的后繼節點可被通知釋放,那么執行CAS操作將head.waitStatus修改為’0’,如果成功,則執行『unparkSuccessor』對head的后繼節點進行釋放操作,如果CAS操作失敗,則說明發送了多線程競爭(即,此時有其他線程也在修改head的waitStatus狀態值),那么重新循環檢查。
② 如果head的waitStatus為’0’,則使用CAS的方式將其修改為’PROPAGATE’。如果CAS操作失敗,則說明發生了多線程競爭,那么重新循環檢查。
③ 如果上面的兩個操作中有一個成功了,就會走到“if (h == head)”這一步,并且此時head節點沒有發生變化,則退出循環,操作結束。否則,說明head節點發生變化了,那么重新循環檢查。
『if (h != null && h != tail)』為false,那么:
說明隊列中沒有等到獲取鎖的節點。會直接到“if (h == head)”,如果此時head節點沒有發生變化,則直接退出循環,操作結束。如果此時head節點發生了變化,那么重新循環檢查。
也就是說,該方法在等待隊列非空時(即,存在一個有效的等待節點,頭結點不是有效節點),會根據head的waitStatus進行后續的操作。
a) 如果『ws == Node.SIGNAL』,則說明需要釋放head后繼節點,如果此時CAS操作『compareAndSetWaitStatus(h, Node.SIGNAL, 0)』也成功的話(說明,此時沒有其他線程在修改head的waitStatus),那么就會執行『unparkSuccessor(h);』來釋放head的后繼節點。
b) 如果『ws != Node.SIGNAL』并且『ws == 0』,則通過CAS操作將head的waitStatus修改為’PROPAGATE’。
以上兩步,當CAS失敗,也就是有其他線程也在修改head的waitStatus狀態時,需要繼續循環進行重新檢測,如果head節點改變了也需要繼續循環重新檢測。

Q:關于node的waitStatus為’0’的情況?
A:當節點不屬于任何waitStatus的話,就會是0。比如,創建好的節點。比如,原來是SIGNAL狀態,在執行完unparkSuccessor操作后(邏輯上說是執行完unparkSuccessor后,但實際的代碼實現必須先將node的waitStatus通過CAS成功從SINGAL修改為0后,才可執行unparkSuccessor操作,以保證多線程競爭情況下的正確性)。比如,將節點從條件隊列轉移到等待隊列的時候,會通過CAS將node的waitStatus從’CONDITION’修改為0。

Q:’PROPAGATE’狀態與釋放之間的關系?
A:當head的waitStatus為’PROPAGATE’的話,在釋放操作時,這個釋放會被廣播下去,也就是說,第一個線程被釋放完后,會繼續釋放第二個被阻塞的線程。。。


『unparkSuccessor』

private void unparkSuccessor(Node node) {
    /*
     * If status is negative (i.e., possibly needing signal) try
     * to clear in anticipation of signalling.  It is OK if this
     * fails or if status is changed by waiting thread.
     */
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * Thread to unpark is held in successor, which is normally
     * just the next node.  But if cancelled or apparently null,
     * traverse backwards from tail to find the actual
     * non-cancelled successor.
     */
    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);
}

喚醒后繼節點,如果存在的話
① 如果狀態值是負數,則在預期發信號通知時清除這個負數狀態值。如果狀態被等待的線程修改了或者清除負數狀態值失敗是允許。
② 后繼節點的線程被喚醒,后繼節點通常就是下一個節點。但是如果下一個節點被取消了或者下一個節點為null,則從隊列尾(tail)往前遍歷去找真實的未取消的后繼節點。
『(s == null || s.waitStatus > 0)』:說明下一個節點為null或被取消了(waitStatus允許的狀態值中,只有’CANCELLED’是>0的)。那么,就從隊列尾(tail)開始向前遍歷,獲取第一個非空且未被取消的節點。如果存在這樣的一個后繼節點的話(即,“s != null”),則執行『LockSupport.unpark(s.thread);』操作來喚醒這個節點的線程。
Q:關于node的waitStatus為’CANCELLED’的情況?
A:關于node的waitStatus為’CANCELLED’的情況:比如,當這個node被中斷了,或者設置的超時時間到了,那么說明這個線程獲取鎖失敗,那么此時就應該將其設置為cancelled,因為如果該線程還需要獲取鎖的話,會重新調用獲取鎖的方法,而獲取鎖的方法就是創建一個新的node的。所以,那么線程獲取鎖失敗的時候就會將這個node的waitStatus設置為’CANCELLED’,一個被取消的線程絕不會獲取鎖成功,一個線程只能被它自己取消,不能被其他線程取消。

Q:關于node為null的情況?
A:關于node為null的情況:比如,一個入隊操作(enq)不會被分配到前驅節點的next字段,直到tail成功指向當前節點之后(通過CAS來將tail指向當前節點。『enq』方法實現中,會先將node.prev = oldTailNode;在需要在CAS成功之后,即tail = node之后,再將oldTailNode.next = node;),所以當看到next字段為null時并不意味著當前節點是隊列的尾部了。無論如何,如果一個next字段顯示為null,我們能夠從隊列尾向前掃描進行復核。


當調用了『LockSupport.unpark(s.thread);』操作后,等待隊列中第一個等待的線程就會重新啟動。流程回到『doAcquireSharedInterruptibly』方法中,線程從阻塞中恢復:

第一個被釋放的線程從『parkAndCheckInterrupt』方法中的『LockSupport.park(this)』掛起結束,繼續后面的流程。因為此時是正常的被喚醒流程,線程并沒有被設置中斷標志,因此『parkAndCheckInterrupt』會返回false。流程重新開始循環。并且通過『Node p = node.predecessor()』為head,接著執行『tryAcquireShared』方法,此時的count==0,所以該方法也會返回’1’,表示獲取共享鎖成功。接著通過『setHeadAndPropagate』將當前節點設置為頭節點并進行廣播如果需要的話。然后將p(即,舊的head節點)的next置null,有助于p被垃圾收集器收集。然后標識failed為false。結束方法調用,返回true。


『setHeadAndPropagate』

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node);
    /*
     * Try to signal next queued node if:
     *   Propagation was indicated by caller,
     *     or was recorded (as h.waitStatus either before
     *     or after setHead) by a previous operation
     *     (note: this uses sign-check of waitStatus because
     *      PROPAGATE status may transition to SIGNAL.)
     * and
     *   The next node is waiting in shared mode,
     *     or we don't know, because it appears null
     *
     * The conservatism in both of these checks may cause
     * unnecessary wake-ups, but only when there are multiple
     * racing acquires/releases, so most need signals now or soon
     * anyway.
     */
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

設置’node’節點為頭結點,并且檢查’node’節點的后繼是否正在等待獲取共享鎖,如果是的話,并且'propagate > 0'或者’node’的waitStatus被設置成了’PROPAGATE’,則廣播。
① 設置’node’為head節點
② 嘗試通知隊列中的下一個節點,如果:
??[1]
???a) 調用者標識了廣播(即,propagate > 0),
???b) 或者waitStatus被前面的操作重新記錄了(’h.waitStatus’可能在setHead之前或之后被重新記錄)(注意,這里使用waitStatus的符號檢查,因為PROPAGATE狀態可能被轉換為SIGNAL)。
??并且[2]隊列中下一個等待的節點是共享模式的,或者下一個節點為null。
這兩次檢查的保守性可能導致不必要的喚醒,但是只有當多線程競爭獲取/釋放鎖時,所以大多數情況下現在或即將需要通知(signal)喚醒。(因為在enq新節點入隊過程中,可能出現next為null的短暫現象,這是發現在節點入隊的過程中,隨后節點就會入隊成功,next字段就不會為null了。所以這里將next為null的情況也考慮了,在廣播釋放時,會將這個正在入隊的節點對應的線程也進行釋放)。
如果符合??[1]、[2]個條件則執行『doReleaseShared()』來釋放后繼的節點。

可設置超時時間的await

『await(long timeout, TimeUnit unit)』同『await()』方法大體是相同的,主要多了在獲取共享鎖時對時間的控制。
在嘗試獲取鎖時的區別:
① 如果傳入的給定的超時納秒數是否小于等于0,如果是則直接返回false,獲取共享鎖失敗。
② 如果在使用自旋的方式獲取共享鎖的過程中,發現已經過了設置的超時時間,那么直接返回false,獲取共享鎖失敗。
③ 如果當前線程無法獲取當共享鎖,并且『shouldParkAfterFailedAcquire』方法返回true(則說明本次獲取共享鎖失敗需要阻塞/掛起當前線程)。但當『nanosTimeout <= spinForTimeoutThreshold』說明設置的超時時間 <= 自旋超時的閾值。這里spinForTimeoutThreshold的值為1000納秒,表示當設置的超時時間小于1000納秒時,使用自旋比使用線程掛起更快。粗略估算這足以去提升響應在一個很短的超時時間內。否則也是使用『LockSupport.parkNanos(this, nanosTimeout);』將當前線程掛起,直到被喚醒或者超時時間到。

取消節點

當嘗試獲取鎖的節點,因為超時或中斷而結束時,說明本次獲取鎖操作失敗,因為本次操作的node就應該被取消。如果線程還需要獲取鎖的話,會再次嘗試獲取鎖操作,此時如果需要的話是會生成一個新的node的。
『cancelAcquire』

private void cancelAcquire(Node node) {
    // Ignore if node doesn't exist
    if (node == null)
        return;

    node.thread = null;

    // Skip cancelled predecessors
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;

    // predNext is the apparent node to unsplice. CASes below will
    // fail if not, in which case, we lost race vs another cancel
    // or signal, so no further action is necessary.
    Node predNext = pred.next;

    // Can use unconditional write instead of CAS here.
    // After this atomic step, other Nodes can skip past us.
    // Before, we are free of interference from other threads.
    node.waitStatus = Node.CANCELLED;

    // If we are the tail, remove ourselves.
    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null);
    } else {
        // If successor needs signal, try to set pred's next-link
        // so it will get one. Otherwise wake it up to propagate.
        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,則直接返回。
② 將node的thread置為null;
③ 將node的prev屬性指向一個在它之前的有效的節點(即,waitStatus <= 0的節點都為有效節點)。 也就是跳過被取消的前驅節點。
④ 『Node predNext = pred.next;』取pred的下一個節點。這個predNext是pred表面上的下一個連接的節點(即,無需考慮該節點是否被取消了)。下面的CAS操作將會失敗(『compareAndSetNext(pred, predNext, null);』or『compareAndSetNext(pred, predNext, next);』),如果和其他的取消或通知操作發生競爭時,這時不需要進一步的操作。因為如果產生競爭,說明pred的next已經被修改了,并且是最新的值了,而我們的操作也就沒有要執行的必要了。
⑤ 將node的waitStatus設置為’CANCELLED’。這里可以使用無條件的寫代替CAS(注意,node的waitStatus是volatile的)。在這個原子操作之后,其他節點會跳過我們(即,跳過waitStatus被置位CANCELLED的節點),在這個原子操作之前,我們不受其他線程的干擾。也就是說,無論其他線程對node的waitStatus是否有在操作,在當前的情況下我們都需要將這個node的waitStatus置為’CANCELLED’。
⑥ 如果待取消的node節點是隊列尾節點的話(即,『node == tail』),那么刪除node自己即可。使用CAS將tail節點設置成前面得到的第一個有效前驅節點(即,『compareAndSetTail(node, pred)』)。并且CAS操作成功的話,執行『compareAndSetNext(pred, predNext, null);』也就是將tail的next置為null的意思。如果該CAS操作失敗的話,沒關系。說明此時tail已經被修改了。
⑦ 如果待取消的node節點不是隊尾節點。并且:
a)pred(即,node的有效前驅節點)不是head節點;并且
b)“pred.waitStatus為SIGNAL” 或者 “pred.waitStatus <= 0”時通過CAS將pred.waitStatus設置為SIGNAL”成功;并且
c) pred的thread非空
那么,當node的next節點非空,且next節點的waitStatus<=0(說明next節點未被取消)時,通過CAS將pred的next執行node的next(即,pred.next = node.next)。同時,如果該CAS操作失敗是沒關系的,說明有其他線程操作已經修改了該pre的next值。
⑧ 如果待取消的node節點不是隊尾節點,并且步驟[7]條件不成立。那么執行『unparkSuccessor(node);』來釋放當前這個待取消節點的下一個節點。(也就是說,當prev是head節點,或者prev也被取消的話,會執行『unparkSuccessor(node);』來釋放node的下一個節點,其實也就是pred的下一個節點)


從上面的分析我們可以知道,其實CountDownLatch中線程的釋放其實是有順序的,根據節點入隊的順序依次被釋放,先入隊的節點的線程會先被釋放。

后記

如果文章有錯不吝指教 :)

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

推薦閱讀更多精彩內容