1. 程式人生 > >多執行緒之CountDownLatch的用法及原理筆記

多執行緒之CountDownLatch的用法及原理筆記

前言-CountDownLatch是什麼?

CountDownLatch是具有synchronized機制的一個工具,目的是讓一個或者多個執行緒等待,直到其他執行緒的一系列操作完成。

CountDownLatch初始化的時候,需要提供一個整形數字,數字代表著執行緒需要呼叫countDown()方法的次數,當計數為0時,執行緒才會繼續執行await()方法後的其他內容。
CountDownLatch(int count);

物件中的方法

getCount:
返回當前的計數count值,
public void countDown()
呼叫此方法後,會減少計數count的值。
遞減後如果為0,則會釋放所有等待的執行緒
public void await()
           throws InterruptedException
呼叫CountDownLatch物件的await方法後。
會讓當前執行緒阻塞,直到計數count遞減至0。

如果當前執行緒數大於0,則當前執行緒線上程排程中將變得不可用,並處於休眠狀態,直到發生以下兩種情況之一:

1、呼叫countDown()方法,將計數count遞減至0。

2、當前執行緒被其他執行緒打斷。

public boolean await(long timeout,
            TimeUnit unit)
              throws InterruptedException

同時await還提供一個帶引數和返回值的方法。

如果計數count正常遞減,返回0後,await方法會返回true並繼續執行後續邏輯。

或是,尚未遞減到0,而到達了指定的時間間隔後,方法返回false。

如果時間小於等於0,則此方法不執行等待。

實際案例

join阻塞等待執行緒完成

首先建立3個執行緒。

public class Worker1 implements Runnable {
    @Override
    public void run() {
        System.out.println("-執行緒1啟動");
        try {
            Thread.sleep(13_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("執行緒1完成--我休眠13秒\r\n");
    }
}

public class Worker2 implements Runnable {
    @Override
    public void run() {
        System.out.println("-執行緒2啟動");
        try {
            Thread.sleep(3_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("執行緒2完成--我休眠3秒\r\n");
    }
}

public class Worker3 implements Runnable {
    @Override
    public void run() {
        System.out.println("-執行緒3啟動");
        try {
            Thread.sleep(3_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


        try {
            Thread.sleep(3_000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("執行緒3完成--我休眠6秒\r\n");
        System.out.println();
    }
}


public class Main {
    public static void main(String[] args) throws InterruptedException {
        Worker1 worker1 = new Worker1();
        Worker2 worker2 = new Worker2();
        Worker3 worker3 = new Worker3();

        Thread thread1 = new Thread(worker1,"執行緒1");
        Thread thread2 = new Thread(worker2,"執行緒2");
        Thread thread3 = new Thread(worker3,"執行緒3");

        thread1.start();
        thread2.start();
        thread3.start();

        thread1.join();
        thread2.join();
        thread3.join();
        System.out.println("主執行緒結束....");

    }
}

列印結果如下:

-執行緒3啟動
-執行緒2啟動
-執行緒1啟動
執行緒2完成--我休眠3秒
執行緒3完成--我休眠6秒

執行緒1完成--我休眠13秒

主執行緒結束....
Process finished with exit code 0

可以看出三個執行緒是並行執行的。啟動順序,並不和執行完畢的順序一致,但可以明確的是,主執行緒為一直阻塞,直到三個執行緒執行完畢。

CountDownLatch用法

阿里巴巴的資料庫連線池Druid中也用了countDownLatch來保證初始化。

// 開啟建立連線的執行緒,如果執行緒池createScheduler為null,
//則開啟單個建立連線的執行緒
createAndStartCreatorThread();  

 // 開啟銷燬過期連線的執行緒
createAndStartDestroyThread(); 

自己編寫一個例子:
這裡模擬一種情況:
主執行緒 依賴 執行緒A初始化三個資料,才能繼續載入後續邏輯。

public class CountDownArticle {
    /**
     * 模擬 主執行緒 依賴 執行緒A初始化一個數據,才能繼續載入後續邏輯
     */
    public static void main(String[] args) throws InterruptedException {
        AtomicReference<String> key = new AtomicReference<>("");
        CountDownLatch countDownLatch = new CountDownLatch(3);
            Thread t = new Thread(() -> {
            try {

                //休眠5秒,模擬資料的初始化
                TimeUnit.SECONDS.sleep(5);

                key.set("核心祕鑰123456");
                System.out.println("資料1初始化完畢");

                //釋放---此處可以在任何位置呼叫,很靈活
                countDownLatch.countDown();

                System.out.println("資料2初始化完畢");
                countDownLatch.countDown();

                System.out.println("資料3初始化完畢");
                countDownLatch.countDown();

            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        });
        t.start();

        //等待資料初始化,阻塞
        countDownLatch.await();
        System.out.println("key:" + key.get());
    }
}

列印內容如下:

資料1初始化完畢
資料2初始化完畢
資料3初始化完畢
key:核心祕鑰123456

CountDownLatch和Join用法的區別?

在使用join()中,多個執行緒只有在執行完畢之後歐才能被解除阻塞,而在CountDownLatch中,執行緒可以在任何時候任何位置呼叫countdown方法減少計數,通過這種方式,我們可以更好地控制執行緒的解除阻塞,而不是僅僅依賴於連線執行緒的完成。

join()方法的執行邏輯如下圖所示:

原理

從原始碼可以看出,CountDownLatch是依賴於AbstractQueuedSynchronizer來實現這一系列邏輯的。

佇列同步器AbstractQueuedSynchronizer
是一個用來構建鎖和同步器的框架,它在內部定義了一個被標識為volatile的名為state的變數,用來表示同步狀態。

多個執行緒之間可以通過AQS來獨佔式或共享式的搶佔資源。

並且它通過內建的FIFO佇列來完成執行緒的排隊工作。

CountDownLatch中的Sync會優先嚐試修改state的值,來獲取同步狀態。例如,如果某個執行緒成功的將state的值從0修改為1,表示成功的獲取了同步狀態。 這個修改的過程是通過CAS完成的,所以可以保證執行緒安全。

反之,如果修改state失敗,則會將當前執行緒加入到AQS的佇列中,並阻塞執行緒。

總結

CountDownLatch(int N) 中的計數器,可以讓我們支援最多等待N個執行緒的操作完成,或是一個執行緒操作N次。

如果僅僅只需要等待執行緒的執行完畢,那麼join可能就能滿足。但是如果需要靈活的控制執行緒,使用CountDownLatch。

注意事項

countDownLatch.countDown();

這一句話儘量寫在finally中,或是保證此行程式碼前的邏輯正常執行,因為在一些情況下,出現異常會導致無法減一,然後出現死鎖。

CountDownLatch 是一次性使用的,當計數值在建構函式中初始化後,就不能再對其設定任何值,當 CountDownLatch 使用完畢,也不能再次被使用。

寫在最後

為了方便大家學習討論,我建立了一個java疑難攻堅互助大家庭,和其他傳統的學習交流不同。本群主要致力於解決專案中的疑難問題,在遇到專案難以解決的
問題時,都可以在這個大家庭裡尋求幫助。

公眾號回覆【問題的答案】進入:java中Integer包裝類的基本資料型別是?
如果你也經歷過遇到專案難題,無從下手,
他人有可能可以給你提供一些思路和看法,一百個人就有一百種思路,
同樣,如果你也樂於幫助別人,那解決別人遇到的問題,也同樣對你是一種鍛鍊。

歡迎來公眾號【俠夢的開發筆記】,回覆乾貨,領取精選學習視訊一份