JUC併發工具--CountDownLatch的使用和原理解析

CountDownLatch 概念

CountDownLatch可以使一個獲多個線程等待其他線程各自執行完畢後再執行

CountDownLatch 定義了一個計數器,和一個阻塞隊列, 當計數器的值遞減爲0之前,阻塞隊列裏面的線程處於掛起狀態,當計數器遞減到0時會喚醒阻塞隊列所有線程,這裏的計數器是一個標誌,可以表示一個任務一個線程,也可以表示一個倒計時器,CountDownLatch可以解決那些一個或者多個線程在執行之前必須依賴於某些必要的前提業務先執行的場景。

CountDownLatch 常用方法說明

//構造方法,創建一個值爲count 的計數器
CountDownLatch(int count);

//阻塞當前線程,將當前線程加入阻塞隊列
await();

//在timeout的時間之內阻塞當前線程,時間一過則當前線程可以執行
await(long timeout, TimeUnit unit);

//對計數器進行遞減1操作,當計數器遞減至0時,當前線程會去喚醒阻塞隊列裏的所有線程
countDown();

CountDownLatch實現原理

1、創建計數器

// 當我們 new CountDownLatch(4) 的時候
CountDownLatch countDownLatch = new CountDownLatch(4);

// 此時會創建一個AQS的同步隊列
// 並把創建CountDownLatch 傳進來的計數器賦值給AQS隊列的 state
// 所以state的值也代表CountDownLatch所剩餘的計數次數
public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);//創建同步隊列,並設置初始計數器值
}

2、阻塞線程

當我們調用countDownLatch.wait()的時候
1. 會創建一個節點,加入到AQS阻塞隊列
2. 並同時把當前線程掛起
public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

1. 判斷計數器是否計數完畢,未完畢則把當前線程加入AQS阻塞隊列
public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
    if (Thread.interrupted()) {
        throw new InterruptedException();
    }
    // 鎖重入次數大於0 則新建節點加入阻塞隊列,掛起當前線程
    if (tryAcquireShared(arg) < 0) {
        doAcquireSharedInterruptibly(arg);
    }
}

2. 構建阻塞隊列的雙向鏈表,掛起當前線程
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
    //新建節點加入阻塞隊列
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            //獲得當前節點pre節點
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);//返回鎖的state
                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);
    }
}

3、計數器遞減

當我們調用countDownLatch.countDown()方法的時候
會對計數器進行減1操作
AQS內部是通過釋放鎖的方式,對state進行減1操作
當state=0的時候證明計數器已經遞減完畢
此時會將AQS阻塞隊列裏的節點線程全部喚醒

public void countDown() {
    //遞減鎖重入次數,當state=0時喚醒所有阻塞線程
    sync.releaseShared(1);
}

1. 對計數器進行減1操作
public final boolean releaseShared(int arg) {
    // 遞減鎖的重入次數,如果當前 state == 0,直接返回
    // 計數器 -1,使用cas方式進行遞減,判斷最新的值是不是 0
    if (tryReleaseShared(arg)) {
        //喚醒隊列所有阻塞的節點
        doReleaseShared();
        return true;
    }
    return false;
}

2. 喚醒所有阻塞隊列裏面的線程
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;
                }
                //成功則喚醒線程
                unparkSuccessor(h);
            } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) {
                continue;
            }
        }
        if (h == head)
            break;
    }
}

參考:

https://zhuanlan.zhihu.com/p/95835099

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