JUC之CountDownLatch原始碼分析

快樂的小樂發表於2020-05-14

CountDownLatch是AbstractQueuedSynchronizer中共享鎖模式的一個的實現,是一個同步工具類,用來協調多個執行緒之間的同步。CountDownLatch能夠使一個或多個執行緒在等待另外一些執行緒完成各自工作之後,再繼續執行。CountDownLatch內部使用一個計數器進行實現執行緒通知條件,計數器初始值為進行通知執行緒的數量。當每一個通知執行緒完成自己任務後,計數器的值就會減一。當計數器的值為0時,表示所有的通知執行緒都已經完成一些任務,然後在CountDownLatch上所有等待的執行緒就可以恢復執行接下來的任務。基本上CountDownLatch的原理就是這樣,下面我們一起去看看原始碼。

public class CountDownLatch {
    
    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;

        Sync(int count) {
            setState(count);
        }
        ....
    }

    private final Sync sync;

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

從上面簡略的原始碼可以看出,CountDownLatch和ReentrantLock一樣,在內部宣告瞭一個繼承AbstractQueuedSynchronizer的Sync內部類(重寫了父類的tryAcquireShared(int acquires)和tryReleaseShared(int releases)),並在宣告瞭一個sync屬性。CountDownLatch只有一個有參構造器,引數count就是上面說的的進行通知的執行緒數目,說白點也就是countDown()方法需要被呼叫的次數。

CountDownLatch的主要方法是wait()和countDown(),我們先看wait()方法原始碼。

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

和ReentrantLock一樣,CountDownLatch依然算是一件外衣,實際還是靠sync進行操作。我們接著看AQS的acquireSharedInterruptibly(int arg)方法(實際上引數在CountDownLatch裡沒什麼用)

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

看到先判斷當前執行緒是否是中斷狀態,然後呼叫子類重寫的tryAcquireShared(int acquires)方法去判斷是否需要進行阻塞(也即是嘗試獲取鎖),如果返回值小於0 ,就呼叫doAcquireSharedInterruptibly(int acquires)方法進行執行緒阻塞。先看tryAcquireShared(int acquires)方法

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

原始碼很簡單,就是看下state是否等於0,等於0,就返回1,代表不需要執行緒阻塞,不等於0(實際上state只會大於或者等於0),就返回-1,表示需要進行執行緒阻塞。這裡有個伏筆就是如果CountDownLatch的計數器state被減至0時,後續再有執行緒呼叫CountDownLatch的wait()方法時,會直接往下執行呼叫者方法的程式碼,不會造成執行緒阻塞

private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

在doAcquireSharedInterruptibly(int acquires)方法中進行執行緒阻塞的步驟依然是先呼叫addWaiter(Node mode)方法將該執行緒封裝到AQS內部的CLH佇列的Node.SHARE(共享)模式的Node節點,並放入到隊尾,然後在迴圈中去嘗試持有鎖和進行執行緒阻塞。在迴圈體內,先獲取到前任隊尾,然後判斷前任隊尾是否是隊首head,如果是就呼叫tryAcquireShared(int acquires)嘗試獲取鎖,如果返回1表示獲取到了鎖,就呼叫setHeadAndPropagate(Node node, int propagate)方法將節點設定head然後再往下傳播,解除後續節點的執行緒阻塞狀態(這就是共享鎖的核心)。如果返回-1,表示沒有獲取到鎖,就呼叫shouldParkAfterFailedAcquire(Node pre, Node node)進行pre節點的waitStatus賦值為Node.SIGNAL,然後在墨跡一次迴圈,呼叫parkAndCheckInterrupt()方法進行執行緒阻塞。我們先看setHeadAndPropagate(Node node, int propagate)方法原始碼

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node);
    
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            // 釋放共享鎖,這是這一步是最關鍵的一步
            doReleaseShared();
    }
}

在setHeadAndPropagate(Node node, int propagate)方法中,直接將node設定從head,因為引數propagate為始終為1(到這一步就表示獲取了共享鎖,state等於0,在tryAcquireShared(int acquires)方法中就只會返回1),所以也就是後面直接獲取head的next節點,如果head的next節點存在,並且是共享模式,就呼叫doReleaseShared()方法去釋放CLH中head的next節點。

shouldParkAfterFailedAcquire(Node pre, Node node)和parkAndCheckInterrupt()兩個方法在JUC之ReentrantLock原始碼分析一篇部落格中已經講過了,這裡不再贅述了。

doReleaseShared()這個方法其實也是countDown()方法的核心實現,這裡先賣個關子,後面會講到。我們接著看doReleaseShared()方法。

private void doReleaseShared() {
    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;
    }
}

 當呼叫wait()方法進行執行緒阻塞等待被別的執行緒解除阻塞後,對於AQS中共享鎖最核心的程式碼就是doReleaseShared()這個方法。先獲取head節點,如果head節點存在並且有後續節點,就先判斷head節點的狀態,如果狀態是Node.SIGNAL(表示後續節點需要鎖釋放通知),將head節點狀態改為0,然後解除下一節點的執行緒阻塞狀態,然後判斷下之前獲取的head和現在的head是否還是同一個,如果是就結束迴圈,如果不是,那就接著迴圈。其實就算是存線上程在執行完unparkSuccessor(Node node)方法後失去了CPU的執行權,一直到被解除執行緒阻塞的next節點坐上了head節點後才有機會獲取到CPU執行權這種情況,無非就是之前獲取到head和現在的head不相同了,大不了再迴圈一次,也算是多執行緒去解除當前head節點的next節點執行緒阻塞,影響不大;如果狀態是0,嘗試將狀態有0改為Node.PROPAGATE,然後重複迴圈,head節點是0的這種狀態在CountDownLatch中應該是不會出現的,可能是AQS對別的類的相容,也可能是我眼拙沒看出來。如果head為null或者head與tail相同,就結束迴圈。

到這裡CountDownLatch的wait()方法就分析完成了,這裡總結下wait()方法流程:
  1、如果state是0,直接往下執行呼叫者的程式碼,不會進行執行緒等待阻塞
  2、將當前執行緒封裝到共享模式的Node節點中,然後放入到CLH佇列的隊尾
  3、將前任隊尾的waitStatus改變為Node.SIGNAL,然後呼叫LockSupport.park()阻塞當前執行緒,等待前節點喚醒
  4、被前節點喚醒後,把自己設為head,然後去喚醒next節點

我們看完了wait()方法,現在在來看下countDown()方法的原始碼

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

一如既往的簡單,直接呼叫AQS的releaseShared(int arg)方法,我們直接去看AQS的releaseShared(int arg)方法

AQS方法
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return 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;
    }
}

在AQS的releaseShared(int arg)中先去呼叫一定要被子類重寫的tryReleaseShared(int releases)方法,返回值表示是否需要進行喚醒操作,如果返回true,那就呼叫doReleaseShared()方法去解除head節點的next節點執行緒阻塞狀態。是的,你沒看錯,就是我們前面分析的doReleaseShared()方法,他們複用了同一個方法。而在CountDownLatch的tryReleaseShared(int releases)方法實現也非常簡單,就是判斷下當前state是否是0,如果是0,表示已經減完了,不需要再減了,等待執行緒已經在被依次喚醒了,返回false表示不需要去喚醒後續節點了。最後再看看減完後的state是否是等於0,等於0,表示需要去解除後續節點的阻塞狀態;不等於0(其實是大於0),表示呼叫countDown()方法去減state的次數還不夠,暫時還不能解除後續節點的阻塞狀態。

countDown()方法比較簡單,我們也總結下countDown()流程:
  1、如果state為0,表示已經有執行緒在解除CLH佇列節點的阻塞狀態了,這裡直接結束
  2、如果state減去1後不等於0,表示還要等有執行緒再次呼叫countDown()方法進行state減1,這裡直接結束
  3、如果state減去1後等於0,表示已經執行緒呼叫countDown()方法的次數已經達到最初設定的次數,然後去解除CLH佇列上節點的阻塞狀態

相關文章