併發工具一之CountDownLatch

一、CountDownLatch簡介

countdownlatch 是一個同步工具類,它允許一個或多個線程一直等待,直到其他線程的操作執行完畢再執行。從命名可以解讀到 countdown 是倒數的意思,類似於我們倒計時的概念。

countdownlatch 提供了兩個方法,一個是 countDown,一個是 await, countdownlatch 初始化的時候需要傳入一個整數,在這個整數倒數到 0 之前,調用了 await 方法的程序都必須要等待直到0纔開始執行,然後通過 countDown 來倒數。

二、使用示例

我們定義一個總數爲3的計數器,t1線程調用await()方法,其他t2/t3/t4線程調用countDown()方法,會發現打印結果總是最後打印t1線程

public class CountDownLatchDemo {
    // 初始化一個總數爲3的計數器
    static CountDownLatch count = new CountDownLatch(3);

    public static void main(String[] args) {
        new Thread(()->{
            try {
                // 調用await()方法,當計數器不爲0的時候,阻塞
                count.await();
                System.out.println("線程t1執行");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"t1").start();

        new Thread(()->{
            // 調用countDown()方法,計數器-1
            count.countDown();
            System.out.println("線程t2執行");
        },"t2").start();

        new Thread(()->{
            // 調用countDown()方法,計數器-1
            count.countDown();
            System.out.println("線程t3執行");
        },"t3").start();

        new Thread(()->{
            // 調用countDown()方法,計數器-1
            count.countDown();
            System.out.println("線程t4執行");
        },"t4").start();
    }
}

打印執行結果:

線程t2執行
線程t4執行
線程t3執行
線程t1執行

Process finished with exit code 0

由於t1阻塞,必須等到其他三個線程執行到計數器爲0的時候纔會執行,所以t1總是最後打印

三、CountDownLatch作用

我們可以使用CountDownLatch做併發模擬,假設我們有100個線程,我們希望他同時在某一時刻同時執行,那麼我們就可以使用CountDownLatch

定義一個線程,調用await()方法直到計數器爲0才執行

public class ThreadDemo extends Thread {

    private CountDownLatch count;

    public ThreadDemo(CountDownLatch count){
        this.count = count;
    }

    @Override
    public void run() {
        try {
            count.await();
            System.out.println("線程"+Thread.currentThread().getName()+"執行");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

測試,循環啓動線程,然後調用countDown()方法,令計數器爲0,然後線程會同時執行

public class CountDownLatchDemo02 {

    static CountDownLatch count = new CountDownLatch(1);

    public static void main(String[] args) {
        for(int i=0;i<10;i++) {
            new ThreadDemo(count).start();
        }
        count.countDown();
    }
}

四、源碼分析

1.
1.構造方法CountDownLatch(int count)

public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        // 定義了一個內部變量sync和內部類Sync,把count傳值到內部類的構造方法
        this.sync = new Sync(count);     //  (1)
    }

我們看一下內部類Sync的實現,發現內部類Sync繼承了AQS,那根據前面對AQS的實現的學習,對此就能比較好了解了

private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;
		// (1) 初始化CountDownLatch調用此方法,把count的值賦給了state,我們學習AQS和ReentrantLock實現的時候知道state是記錄鎖的,state>0表示鎖的次數
        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;
            }
        }
    }

2.await()方法

public void await() throws InterruptedException {
		// await是調用AQS的acquireSharedInterruptibly方法加鎖,從方法名字可以看出,這個實現是一種共享鎖
        sync.acquireSharedInterruptibly(1);
    }
public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        // 判斷是否被中斷過,如果中斷過就跑出異常
        if (Thread.interrupted())
            throw new InterruptedException();
        //state 如果不等於 0,說明當前線程需要加入到共享鎖隊列中
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
    }
protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }

doAcquireSharedInterruptibly添加鎖的過程

  1. addWaiter 設置爲 shared 模式。
  2. tryAcquire 和 tryAcquireShared 的返回值不同,因此會多出一個判斷過程
  3. 在 判 斷 前 驅 節 點 是 頭 節 點 後 , 調 用 了setHeadAndPropagate 方法,而不是簡單的更新一下頭節點。
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) {
                // 如果前一個是head節點,就判斷嘗試獲取鎖
                    int r = tryAcquireShared(arg);
                    //r>=0 表示獲取到了執行權限,這個時候因爲 state!=0,所以不會執行這段代碼
                    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);
        }
    }

setHeadAndPropagate方法
這個方法的主要作用是把被喚醒的節點,設置成 head 節點。 然後繼續喚醒隊列中的其他線程。

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

由於線程被 await 方法阻塞了,所以只有等到countdown 方法使得 state=0 的時候纔會被喚醒,我們來看看 countdown 做了什麼

3.countDown()方法

public void countDown() {
        sync.releaseShared(1);
    }
  1. 只有當 state 減爲 0 的時候,tryReleaseShared 才返回 true, 否則只是簡單的 state = state - 1
  2. 如果 state=0, 則調用 doReleaseShared喚醒處於 await 狀態下的線程
public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }
protected boolean tryReleaseShared(int releases) {
            // 用自旋的方法實現 state 減 1
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }

AQS. doReleaseShared方法
共享鎖的釋放和獨佔鎖的釋放有一定的差別,前面喚醒鎖的邏輯和獨佔鎖是一樣,先判斷頭結點是不是SIGNAL 狀態,如果是,則修改爲 0,並且喚醒頭結點的下一個節點
PROPAGATE : 標識爲 PROPAGATE 狀態的節點,是共享鎖模式下的節點狀態,處於這個狀態下的節點,會對線程的喚醒進行傳播

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);
                }
                // 這個 CAS 失敗的場景是:執行到這裏的時候,剛好有一個節點入隊,入隊會將這個 ws 設置爲 -1
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            // 如果到這裏的時候,前面喚醒的線程已經佔領了 head,那麼再循環
			// 通過檢查頭節點是否改變了,如果改變了就繼續循環
            if (h == head)                   // loop if head changed
                break;
        }
    }

一旦 線程被喚醒,代碼又會繼續回到doAcquireSharedInterruptibly 中來執行。如果當前 state滿足=0 的條件,則會執行 setHeadAndPropagate 方法

五、總結

1.從代碼的實現來看,有點類似 join 的功能,但是比 join 更加靈活。CountDownLatch 構造函數會接收一個 int 類型的參數作爲計數器的初始值,當調用 CountDownLatch 的countDown 方法時,這個計數器就會減一。通過 await 方法去阻塞主流程
2.CountDownLatch的實現是基於共享鎖機制實現的

本文是綜合自己的認識和參考各類資料(書本及網上資料)編寫,若有侵權請聯繫作者,所有內容僅代表個人認知觀點,如有錯誤,歡迎校正; 郵箱:[email protected] 博客地址:https://blog.csdn.net/qq_35576976/

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