面試官:實戰中用過CountDownLatch嗎?詳細說一說,我:啊這

JavaBuild發表於2024-04-14

寫在開頭

在很多的面經中都看到過提問 CountDownLatch 的問題,正好我們最近也在梳理學習AQS(抽象佇列同步器),而CountDownLatch又是其中典型的代表,我們今天就繼續來學一下這個同步工具類!

CountDownLatch有何作用?

我們知道AQS是專屬於構造鎖和同步器的一個抽象工具類,基於它Java構造出了大量的常用同步工具,如ReentrantLock、Semaphore、ReentrantReadWriteLock、SynchronousQueue等等,我們今天的主角CountDownLatch同樣如此。

CountDownLatch(倒時器)允許N個執行緒阻塞在同一個地方,直至所有執行緒的任務都執行完畢。CountDownLatch 有一個計數器,可以透過countDown()方法對計數器的數目進行減一操作,也可以透過await()方法來阻塞當前執行緒,直到計數器的值為 0。

CountDownLatch的底層原理

想要迅速瞭解一個Java類的內部構造,或者使用原理,最快速直接的辦法就是看它的原始碼,這是很多初學者比較牴觸的,會覺得很多封裝起來的原始碼都晦澀難懂,誠然很多類內部實現是複雜,但我們作為Java工程師也不能只追求CRUD呀,培養自己看原始碼的習慣,硬著頭皮看段時間,程式碼能力絕對會提升的!

廢話說的有點多了,我們直接進入CountDownLatch內部去看看它的底層原理吧

【原始碼解析1】

//幾乎所有基於AQS構造的同步類,內部都需要一個靜態內部類去繼承AQS
private static final class Sync extends AbstractQueuedSynchronizer {
     private static final long serialVersionUID = 4982264981922014374L;
     Sync(int count) {
         setState(count);
     }
     int getCount() {
         return getState();
     }
 }
private final Sync sync;
//構造方法中初始化count值
public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}

幾乎所有基於AQS構造的同步類,內部都需要一個靜態內部類去繼承AQS,並實現其提供的鉤子方法,透過封裝AQS中的state為count來確定多個執行緒的計時器。

countDown()方法

【原始碼解析2】

//核心方法,內部封裝了共享模式下的執行緒釋放
 public void countDown() {
 	//內部類Sync,繼承了AQS
     sync.releaseShared(1);
 }
 //AQS內部的實現
 public final boolean releaseShared(int arg) {
  if (tryReleaseShared(arg)) {
  		//喚醒後繼節點
         doReleaseShared();
         return true;
     }
     return false;
 }   

在CountDownLatch中透過countDown來減少倒數計時數,這是最重要的一個方法,我們繼續跟進原始碼看到它透過releaseShared()方法去釋放鎖,這個方法是AQS內部的預設實現方法,而在這個方法中再一次的呼叫了tryReleaseShared(arg),這是一個AQS的鉤子方法,方法內部僅有預設的異常處理,真正的實現由CountDownLatch內部類Sync完成

image

【原始碼解析3】

// 對 state 進行遞減,直到 state 變成 0;
// 只有 count 遞減到 0 時,countDown 才會返回 true
protected boolean tryReleaseShared(int releases) {
    // 自選檢查 state 是否為 0
    for (;;) {
        int c = getState();
        // 如果 state 已經是 0 了,直接返回 false
        if (c == 0)
            return false;
        // 對 state 進行遞減
        int nextc = c-1;
        // CAS 操作更新 state 的值
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

await()方法

除了countDown()方法外,在CountDownLatch中還有一個重要方法就是 await ,在多執行緒環境下,執行緒的執行順序並不一致,因此,對於一個倒時器也說,先開始的執行緒應該阻塞等待直至最後一個執行緒執行完成,而實現這一效果的就是await()方法!

【原始碼解析4】

// 等待(也可以叫做加鎖)
public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}
// 帶有超時時間的等待
public boolean await(long timeout, TimeUnit unit)
    throws InterruptedException {
    return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

其中await()方法可以配置帶有時間引數的,表示最大阻塞時間,當呼叫 await() 的時候,如果 state 不為 0,那就證明任務還沒有執行完畢,await() 就會一直阻塞,也就是說 await() 之後的語句不會被執行。然後,CountDownLatch 會自旋 CAS 判斷 state是否等於0,若是就會釋放所有等待的執行緒,await() 方法之後的語句得到執行。

CountDownLatch的使用

由於await的實現步驟和countDown類似,我們就不貼原始碼了,大家自己跟進去也很容易看明白,我們現在直接來一個小demo感受一下如何使用CountDownLatch做一個倒時器

【程式碼樣例1】

public class Test {
    public static void main(String[] args) throws InterruptedException {
        // 建立一個倒計數為 3 的 CountDownLatch
        CountDownLatch latch = new CountDownLatch(3);

        Thread service1 = new Thread(new Service("3", 1000, latch));
        Thread service2 = new Thread(new Service("2", 2000, latch));
        Thread service3 = new Thread(new Service("1", 3000, latch));

        service1.start();
        service2.start();
        service3.start();

        // 等待所有服務初始化完成
        latch.await();
        System.out.println("發射");
    }

    static class Service implements Runnable {
        private final String name;
        private final int timeToStart;
        private final CountDownLatch latch;

        public Service(String name, int timeToStart, CountDownLatch latch) {
            this.name = name;
            this.timeToStart = timeToStart;
            this.latch = latch;
        }

        @Override
        public void run() {
            try {
                Thread.sleep(timeToStart);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(name);
            // 減少倒計數
            latch.countDown();
        }
    }
}

輸出:

3
2
1
發射

執行結果體現出了倒數計時的效果每隔1秒進行3,2,1的倒數;其實除了倒數計時器外CountDownLatch還有另外一個使用場景:實現多個執行緒開始執行任務的最大並行性

多個執行緒在某一時刻同時開始執行。類似於賽跑,將多個執行緒放到起點,等待發令槍響,然後同時開跑。

具體做法是: 初始化一個共享的 CountDownLatch 物件,將其計數器初始化為 1 (new CountDownLatch(1)),多個執行緒在開始執行任務前首先 coundownlatch.await(),當主執行緒呼叫 countDown() 時,計數器變為 0,多個執行緒同時被喚醒。

【程式碼樣例2】

public class Test {
    public static void main(String[] args) throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    System.out.println("5位運動員就位!");
                    //等待發令槍響
                    countDownLatch.await();

                    System.out.println(Thread.currentThread().getName() + "起跑!");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
        // 裁判準備發令
        Thread.sleep(2000);
        //發令槍響
        countDownLatch.countDown();
    }
}

輸出:

5位運動員就位!
5位運動員就位!
5位運動員就位!
5位運動員就位!
5位運動員就位!
Thread-0起跑!
Thread-3起跑!
Thread-4起跑!
Thread-1起跑!
Thread-2起跑!

結尾彩蛋

如果本篇部落格對您有一定的幫助,大家記得留言+點贊+收藏呀。原創不易,轉載請聯絡Build哥!

image

如果您想與Build哥的關係更近一步,還可以關注“JavaBuild888”,在這裡除了看到《Java成長計劃》系列博文,還有提升工作效率的小筆記、讀書心得、大廠面經、人生感悟等等,歡迎您的加入!

image

相關文章