深入理解CountDownLatch原理

                                      深入理解CountDownLatch原理

一、簡介

CountDownLatch是一個同步工具類,它允許一個或多個線程一直等待,直到其他線程執行完後再執行。例如,應用程序的主線程希望在負責啓動框架服務的線程已經啓動所有框架服務之後執行。CountDownLatch是在java1.5被引入的,跟它一起被引入的併發工具類還有CyclicBarrier、Semaphore、ConcurrentHashMap和BlockingQueue等,它們都存在於java.util.concurrent包下。

二、源碼 

函數列表

//構造一個用給定計數初始化的 CountDownLatch。
CountDownLatch(int count)
// 使當前線程在鎖存器倒計數至零之前一直等待,除非線程被中斷。
void await()
// 使當前線程在鎖存器倒計數至零之前一直等待,除非線程被中斷或超出了指定的等待時間。
boolean await(long timeout, TimeUnit unit)
// 遞減鎖存器的計數,如果計數到達零,則釋放所有等待的線程。
void countDown()
// 返回當前計數。
long getCount()
// 返回標識此鎖存器及其狀態的字符串。
String toString()

數據結構

CountDownLatch的UML類圖如下:

CountDownLatch的數據結構很簡單,它是通過"共享鎖"實現的。它包含了sync對象,sync是Sync類型。Sync是實例類,它繼承於AQS。

package java.util.concurrent;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
 
public class CountDownLatch {
    /**
     * Synchronization control For CountDownLatch.
     * Uses AQS state to represent count.
     */
    private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;
 
        Sync(int count) {
            setState(count);
        }
 
        int getCount() {
            return getState();
        }
 
        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }
 
        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;
            }
        }
    }
 
    private final Sync sync;
 
    /**
     * Constructs a {@code CountDownLatch} initialized with the given count.
     *
     * @param count the number of times {@link #countDown} must be invoked
     *        before threads can pass through {@link #await}
     * @throws IllegalArgumentException if {@code count} is negative
     */
    public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        this.sync = new Sync(count);
    }
 
    /**
     * Causes the current thread to wait until the latch has counted down to
     * zero, unless the thread is {@linkplain Thread#interrupt interrupted}.
     *
     * <p>If the current count is zero then this method returns immediately.
     *
     * <p>If the current count is greater than zero then the current
     * thread becomes disabled for thread scheduling purposes and lies
     * dormant until one of two things happen:
     * <ul>
     * <li>The count reaches zero due to invocations of the
     * {@link #countDown} method; or
     * <li>Some other thread {@linkplain Thread#interrupt interrupts}
     * the current thread.
     * </ul>
     *
     * <p>If the current thread:
     * <ul>
     * <li>has its interrupted status set on entry to this method; or
     * <li>is {@linkplain Thread#interrupt interrupted} while waiting,
     * </ul>
     * then {@link InterruptedException} is thrown and the current thread's
     * interrupted status is cleared.
     *
     * @throws InterruptedException if the current thread is interrupted
     *         while waiting
     */
    public void await() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }
 
    /**
     * Causes the current thread to wait until the latch has counted down to
     * zero, unless the thread is {@linkplain Thread#interrupt interrupted},
     * or the specified waiting time elapses.
     *
     * <p>If the current count is zero then this method returns immediately
     * with the value {@code true}.
     *
     * <p>If the current count is greater than zero then the current
     * thread becomes disabled for thread scheduling purposes and lies
     * dormant until one of three things happen:
     * <ul>
     * <li>The count reaches zero due to invocations of the
     * {@link #countDown} method; or
     * <li>Some other thread {@linkplain Thread#interrupt interrupts}
     * the current thread; or
     * <li>The specified waiting time elapses.
     * </ul>
     *
     * <p>If the count reaches zero then the method returns with the
     * value {@code true}.
     *
     * <p>If the current thread:
     * <ul>
     * <li>has its interrupted status set on entry to this method; or
     * <li>is {@linkplain Thread#interrupt interrupted} while waiting,
     * </ul>
     * then {@link InterruptedException} is thrown and the current thread's
     * interrupted status is cleared.
     *
     * <p>If the specified waiting time elapses then the value {@code false}
     * is returned.  If the time is less than or equal to zero, the method
     * will not wait at all.
     *
     * @param timeout the maximum time to wait
     * @param unit the time unit of the {@code timeout} argument
     * @return {@code true} if the count reached zero and {@code false}
     *         if the waiting time elapsed before the count reached zero
     * @throws InterruptedException if the current thread is interrupted
     *         while waiting
     */
    public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }
 
    /**
     * Decrements the count of the latch, releasing all waiting threads if
     * the count reaches zero.
     *
     * <p>If the current count is greater than zero then it is decremented.
     * If the new count is zero then all waiting threads are re-enabled for
     * thread scheduling purposes.
     *
     * <p>If the current count equals zero then nothing happens.
     */
    public void countDown() {
        sync.releaseShared(1);
    }
 
    /**
     * Returns the current count.
     *
     * <p>This method is typically used for debugging and testing purposes.
     *
     * @return the current count
     */
    public long getCount() {
        return sync.getCount();
    }
 
    /**
     * Returns a string identifying this latch, as well as its state.
     * The state, in brackets, includes the String {@code "Count ="}
     * followed by the current count.
     *
     * @return a string identifying this latch, as well as its state
     */
    public String toString() {
        return super.toString() + "[Count = " + sync.getCount() + "]";
    }
}

CountDownLatch是通過“共享鎖”實現的。下面,我們分析CountDownLatch中3個核心函數: CountDownLatch(int count), await(), countDown()

構造函數

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

說明:該函數是創建一個Sync對象,而Sync是繼承於AQS類。Sync構造函數如下:

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

setState()在AQS中實現,源碼如下:

protected final void setState(long newState) {
    state = newState;
}

 說明:在AQS中,state是一個private volatile long類型的對象。對於CountDownLatch而言,state表示的”鎖計數器“。CountDownLatch中的getCount()最終是調用AQS中的getState(),返回的state對象,即”鎖計數器“。
await()函數

public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}
//說明:該函數實際上是調用的AQS的acquireSharedInterruptibly(1);
 
public final void acquireSharedInterruptibly(long arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}

說明:acquireSharedInterruptibly()的作用是獲取共享鎖。

如果當前線程是中斷狀態,則拋出異常InterruptedException。否則,調用tryAcquireShared(arg)嘗試獲取共享鎖;嘗試成功則返回,否則就調用doAcquireSharedInterruptibly()。doAcquireSharedInterruptibly()會使當前線程一直等待,直到當前線程獲取到共享鎖(或被中斷)才返回。

tryAcquireShared()在CountDownLatch.java中被重寫,它的源碼如下:

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

說明:tryAcquireShared()的作用是嘗試獲取共享鎖。
如果"鎖計數器=0",即鎖是可獲取狀態,則返回1;否則,鎖是不可獲取狀態,則返回-1。

private void doAcquireSharedInterruptibly(long arg)
    throws InterruptedException {
    // 創建"當前線程"的Node節點,且Node中記錄的鎖是"共享鎖"類型;並將該節點添加到CLH隊列末尾。
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            // 獲取上一個節點。
            // 如果上一節點是CLH隊列的表頭,則"嘗試獲取共享鎖"。
            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;
                }
            }
            // (上一節點不是CLH隊列的表頭) 當前線程一直等待,直到獲取到共享鎖。
            // 如果線程在等待過程中被中斷過,則再次中斷該線程(還原之前的中斷狀態)。
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

說明:

a. addWaiter(Node.SHARED)的作用是,創建”當前線程“的Node節點,且Node中記錄的鎖的類型是”共享鎖“(Node.SHARED);並將該節點添加到CLH隊列末尾。關於Node和CLH在深入理解AQS(AbstractQueuedSynchronizer)​​​​​已經詳細介紹過,這裏就不再重複說明了。
b. node.predecessor()的作用是,獲取上一個節點。如果上一節點是CLH隊列的表頭,則”嘗試獲取共享鎖“。
c. shouldParkAfterFailedAcquire()的作用和它的名稱一樣,如果在嘗試獲取鎖失敗之後,線程應該等待,則返回true;否則,返回false。
d. 當shouldParkAfterFailedAcquire()返回ture時,則調用parkAndCheckInterrupt(),當前線程會進入等待狀態,直到獲取到共享鎖才繼續運行。
doAcquireSharedInterruptibly()中的shouldParkAfterFailedAcquire(), parkAndCheckInterrupt等函數在深入理解AQS(AbstractQueuedSynchronizer)中介紹過,這裏也就不再詳細說明了。

再簡述:await()方法是怎麼“阻塞”當前線程的,已經非常明白了。其實說白了,就是當你調用了countDownLatch.await()方法後,你當前線程就會進入了一個死循環當中,在這個死循環裏面,會不斷的進行判斷,通過調用tryAcquireShared方法,不斷判斷我們上面說的那個計數器,看看它的值是否爲0了(爲0的時候,其實就是我們調用了足夠多次數的countDownLatch.countDown()方法的時候),如果是爲0的話,tryAcquireShared就會返回1,然後跳出了循環,也就不再“阻塞”當前線程了。

countDown()函數

public void countDown() {
    sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
    // 只有當 state 減爲 0 的時候,tryReleaseShared 才返回 true
    // 否則只是簡單的 state = state - 1 那麼 countDown 方法就結束了
    if (tryReleaseShared(arg)) {
        // 喚醒 await 的線程
        doReleaseShared();
        return true;
    }
    return false;
}
// 這個方法很簡單,用自旋的方法實現 state 減 1
protected boolean tryReleaseShared(int releases) {
    for (;;) {
        int c = getState();
        if (c == 0)
            return false;
        int nextc = c-1;
        //通過CAS將state的值減1,失敗就不會進入return,繼續for循環,直至CAS成功
        if (compareAndSetState(c, nextc))
            //state減到0就返回true,否則返回false
            return nextc == 0;
    }
}

countDown 方法就是每次調用都將 state 值減 1,如果 state 減到 0 了,那麼就調用下面的方法進行喚醒阻塞隊列中的線程:

// 調用這個方法的時候,state == 0
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
                // 就是這裏,喚醒 head 的後繼節點,也就是阻塞隊列中的第一個節點
                // 在這裏,也就是喚醒線程 ,可以接着運行了
                unparkSuccessor(h);
            }
            else if (ws == 0 && 
            !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) 
                continue;                // loop on failed CAS
        }
      
        if (h == head)                   // loop if head changed
            break;
    }
}

我們繼續回到 await 的這段代碼,在第24行代碼 parkAndCheckInterrupt 返回繼續接着運行,我們先不考慮中斷的情況: 

private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            //p表示當前節點的前驅節點
            final Node p = node.predecessor();
            //此時被喚醒的是之前head的後繼節點,所以此線程的前驅節點是head
            if (p == head) {
                //此時state已經爲0,r爲1
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    // 2. 這裏將喚醒的後續節點,以此類推,後續節點被喚醒後,會在它的await中喚醒t4的後續節點
                    setHeadAndPropagate(node, r); 
                    // 將已經喚醒的t3節點從隊列中去除
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                // 1. 喚醒後這個方法返回
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

會循環一次進到 setHeadAndPropagate(node, r) 這個方法,先把 head 給佔了,然後喚醒隊列中其他的線程: 

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node);
 
    // 下面說的是,喚醒當前 node 之後的節點,即當前後續節點已經醒了,馬上喚醒之後後續節點
    // 類似的,如果後面還有節點,那麼醒了以後,馬上將後面的節點給喚醒了
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            // 又是這個方法,只是現在的 head 已經不是原來的空節點了,是當前的節點了
            doReleaseShared();
    }
}

又回到這個方法了,那麼接下來,我們好好分析 doReleaseShared 這個方法,我們根據流程

// 調用這個方法的時候,state == 0
private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            // 將頭節點的 waitStatus 設置爲 Node.SIGNAL(-1) 了
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                // 就是這裏,喚醒 head 的後繼節點,也就是阻塞隊列中的第一個節點
                // 在這裏,也就是喚醒後續節點
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                     // 這個 CAS 失敗的場景是:執行到這裏的時候,剛好有一個節點入隊,入隊會將這個 ws 設置爲 -1
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        // 如果到這裏的時候,前面喚醒的線程已經佔領了 head,那麼再循環
        // 否則,就是 head 沒變,那麼退出循環,
        // 退出循環是不是意味着阻塞隊列中的其他節點就不喚醒了?當然不是,喚醒的線程之後還是會在await()方法中調用此方法接着喚醒後續節點
        if (h == head)                   // loop if head changed
            break;
    }
}

總結:CountDownLatch是通過“共享鎖”實現的。在創建CountDownLatch中時,會傳遞一個int類型參數count,該參數是“鎖計數器”的初始狀態,表示該“共享鎖”最多能被count給線程同時獲取。當某線程調用該CountDownLatch對象的await()方法時,該線程會等待“共享鎖”可用時,才能獲取“共享鎖”進而繼續運行。而“共享鎖”可用的條件,就是“鎖計數器”的值爲0!而“鎖計數器”的初始值爲count,每當一個線程調用該CountDownLatch對象的countDown()方法時,纔將“鎖計數器”-1;通過這種方式,必須有count個線程調用countDown()之後,“鎖計數器”才爲0,而前面提到的等待線程才能繼續運行!以上,就是CountDownLatch的實現原理。
三、示例

import java.util.Random;
import java.util.concurrent.CountDownLatch;

public class Test {

    private static final int THREAD_COUNT_NUM = 7;
    private static CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT_NUM);

    public static void main(String[] args) throws InterruptedException {

        for (int i = 1; i <= THREAD_COUNT_NUM; i++) {
            int index = i;
            new Thread(() -> {
                try {
                    System.out.println("第" + index + "顆龍珠已收集到!");
                    //模擬收集第i個龍珠,隨機模擬不同的尋找時間
                    Thread.sleep(new Random().nextInt(3000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //每收集到一顆龍珠,需要等待的顆數減1
                countDownLatch.countDown();
            }).start();
        }
        //等待檢查,即上述7個線程執行完畢之後,執行await後邊的代碼
        countDownLatch.await();
        System.out.println("集齊七顆龍珠!召喚神龍!");
    }
}

CountDownLatch和CyclicBarrier的區別
(01) CountDownLatch的作用是允許1或N個線程等待其他線程完成執行;而CyclicBarrier則是允許N個線程相互等待。
(02) CountDownLatch的計數器無法被重置;CyclicBarrier的計數器可以被重置後使用,因此它被稱爲是循環的barrier。
 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章