同步工具類—— CountDownLatch

程式設計師自由之路發表於2020-04-17

本部落格系列是學習併發程式設計過程中的記錄總結。由於文章比較多,寫的時間也比較散,所以我整理了個目錄貼(傳送門),方便查閱。

併發程式設計系列部落格傳送門

CountDownLatch簡介

CountDownLatch是JDK併發包中提供的一個同步工具類。官方文件對這個同步工具的介紹是:

A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.

上面的英文介紹大致意思是:CountDownLatch的主要功能是讓一個或者多個執行緒等待直到一組在其他執行緒中執行的操作完成。

使用列子

觀看上面的解釋可能並不能直觀的說明CountDownLatch的作用,下面我們通過一個簡單的列子看下CountDownLatch的使用。

場景:主人(主執行緒)請客人(子執行緒)吃晚飯,需要等待所有客人都到了之後才開飯。我們用CountDownLatch來模擬下這個場景。

public class CountDownLatchDemo {

    private static final int PERSON_COUNT = 5;

    private static final CountDownLatch c = new CountDownLatch(PERSON_COUNT);

    public static void main(String[] args) throws InterruptedException {
        System.out.println("l am master, waiting guests...");
        for (int i = 0; i < PERSON_COUNT; i++) {
            int finalI = i;
            new Thread(new Runnable() {
                @SneakyThrows
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName()+" l am person["+ finalI +"]");
                    TimeUnit.MILLISECONDS.sleep(500);
                    //System.out.println(Thread.currentThread().getName()+" count:"+c.getCount());
                    c.countDown();
                }
            }).start();
        }
        c.await();
        System.out.println("all guests get, begin dinner...");
    }

}

上面的列子中,主人(master執行緒)請了5個客人吃飯,每個客人到了之後會將CountDownLatch的值減一,主人(master)會一直等待所有客人到來,最後輸出”開飯“。

CountDownLatch的使用方式很簡單,下面來看下它的實現原理。

原理剖析

首先我們先看下CountDownLatch重要的API

- getCount():獲取當前count的值。
- wait():讓當前執行緒在此CountDownLatch物件上等待,可以中斷。與notify()、notifyAll()方法對應。
- await():讓當前執行緒等待此CountDownLatch物件的count變為0,可以中斷。
- await(timeout,TimeUnit):讓當前執行緒等待此CountDownLatch物件的count變為0,可以超時、可以中斷。
- countDown():使此CountDownLatch物件的count值減1(無論執行多少次,count最小值為0)。

下面我們看下具體API的原始碼

建構函式

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

在構建CountDownLatch物件時需要傳入一個int型的初始值,這個值就是計數器的初始值。從上面的程式碼中可以看出,建立CountDownLatch是new了一個Sync物件。

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

Sync物件是基於AQS機制實現的,自己實現了tryAcquireSharedtryReleaseShared方法。

await方法


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

呼叫await方法其實是呼叫了AQS的acquireSharedInterruptibly方法。

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

acquireSharedInterruptibly中先判斷了下當前執行緒有沒有被中斷,假如執行緒已經被中斷了,直接丟擲中斷異常。否則進入doAcquireSharedInterruptibly

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的處理邏輯是先判斷佇列中是否只有當前執行緒,如果只有當前執行緒的先嚐試獲取下資源,如果獲取資源成功就直接返回了。獲取資源不成功就判斷下是否要park當前執行緒,如果需要park當前執行緒,
那麼當前執行緒就進入waiting狀態。否則在for迴圈中一直執行上面的邏輯。

countDown方法

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

熟悉AQS機制的會知道上面的程式碼其實也是調的AQS的releaseSharedreleaseShared的方法會調到Sync中的tryReleaseShared

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

上面的程式碼邏輯很簡單:status的值是0的話就返回true,否則返回false。返回true的話,就會喚醒AQS佇列中所有阻塞的執行緒。

使用場景

  • 場景一:將任務分割成多個子任務,每個子任務由單個執行緒去完成,等所有執行緒完成後再將結果彙總。(MapReduce)這種場景下,CountDoenLatch作為一個完成訊號來使用。
  • 場景二:多個執行緒等到,一直等到某個條件發生。比如多個賽跑運動員都做好了準備,就等待裁判手中的發令槍響。這種場景下,就可以將CountdownLatch的初始值設定成1。

簡單總結

  • CountDownLatch的初始值不能重置,只能減少不能增加,最多減少到0;
  • CountDownLatch計數值沒減少到0之前,呼叫await方法可能會讓呼叫執行緒進組一個阻塞佇列,等待計數值減小到0;
  • 呼叫countDown方法會讓計數值每次都減小1,但是最多減少到0。當CountDownLatch的計數值減少到0的時候,會喚醒所有在阻塞佇列中的執行緒。

相關文章