Java并發(fā)編程 - CountDownLatch

在我參閱的眾多的書籍當(dāng)中,都沒有看到對這個(gè)類名的翻譯,可能是覺得沒有一個(gè)更好的中文單詞從字面上描述這個(gè)類名,又或者有合適的但是正確不能表達(dá)它要表達(dá)的含義。

JDK API的書寫者既然用這個(gè)類名,想其他大多數(shù)類名一樣,肯定是希望達(dá)到望文知義的效果。現(xiàn)在我們試圖從這個(gè)命名出發(fā)來描述它要表達(dá)的含義。

CountDownLatch = count down + latch

count down: 計(jì)數(shù)減小
latch: 門閂——指門關(guān)上后,插在門內(nèi)使門推不開的滑動(dòng)插銷。

幾乎所有的文章都把我這里的"計(jì)數(shù)減小"描述成"倒計(jì)時(shí)"。不過因?yàn)槲覀兊某WR(shí),“倒計(jì)時(shí)”跟時(shí)間有關(guān)系,我們所看到的"倒計(jì)時(shí)"是自動(dòng)地,可能就會(huì)覺得這個(gè)CountDownLatch類也有這種自動(dòng)倒計(jì)時(shí)的功能,有這樣自覺地話對我們理解和使用這個(gè)類就有影響,所以這里我不把它寫成倒計(jì)時(shí)。

那么從字面上翻譯就是——計(jì)數(shù)減小門閂。

這個(gè)看上去很生硬,的確是這樣。

那么我們可不可以這樣望文生義一下:這個(gè)CountDownLatch提供了兩個(gè)功能,一個(gè)就是計(jì)數(shù)減小功能,另一個(gè)就是門閂功能。

計(jì)數(shù)減小功能:對于這個(gè)功能,相信大多數(shù)人都是很清楚如何實(shí)現(xiàn)的。比如說倒計(jì)時(shí)60秒,那么第一步首先規(guī)定有60秒,第二步開始不斷地減秒數(shù)。

門閂功能:門閂的作用是關(guān)住門,阻止別人再進(jìn)來。關(guān)注門是要做的動(dòng)作,阻止別人再進(jìn)來是目的。

有的人覺得還應(yīng)該有一個(gè)打開門閂的功能,我們暫且猜測,這是個(gè)智能門閂,計(jì)數(shù)變?yōu)?之后門閂自動(dòng)打開。

通過研究和使用CountDownLatch類,我們發(fā)現(xiàn)它提供的功能與我們猜測相符。

計(jì)數(shù)總數(shù)設(shè)置
CountDownLatch通過提供可接受倒計(jì)時(shí)總數(shù)作為參數(shù)的構(gòu)造方法實(shí)現(xiàn)倒計(jì)時(shí)總數(shù)設(shè)置。這個(gè)計(jì)數(shù)總數(shù)通常都是具有某種業(yè)務(wù)含義的數(shù)字。

public CountDownLatch(int count) 

計(jì)數(shù)減數(shù)
下面的方法提供計(jì)數(shù)減數(shù)的功能,每次減1。

public void countDown() 

上門閂

public void await() 

現(xiàn)在我們通過源碼來逐個(gè)分析上面的功能。

計(jì)數(shù)總數(shù)設(shè)置

上面提到了是通過CountDownLatch的構(gòu)造方法設(shè)置的,這里因?yàn)樯婕暗紸QS的知識(shí),所以我們不深入源碼,只要知道這個(gè)類擁有一個(gè)表示計(jì)數(shù)的屬性。

private volatile int state;

計(jì)數(shù)減數(shù)

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

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

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

在我們上面設(shè)置的計(jì)數(shù)總數(shù)的基礎(chǔ)上減1,計(jì)數(shù)總數(shù)變?yōu)闇p1后的值。

上門閂

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

這個(gè)方法會(huì)檢查當(dāng)前的計(jì)數(shù)是否為0,如果計(jì)數(shù)不為0那么就表示上門閂成功,await()后的代碼被門閂擋住,無法繼續(xù)執(zhí)行,當(dāng)前的線程會(huì)被掛起,直到計(jì)數(shù)為0,線程被喚醒繼續(xù)執(zhí)行。

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

打開門閂

上面我們提到"打開門閂"這個(gè)功能,而且我們猜測是自動(dòng)的,看來確實(shí)是這樣,因?yàn)镃ountDownLatch這個(gè)類沒有為我們提供打開門閂這個(gè)方法。

現(xiàn)在我們來研究一下,什么時(shí)候會(huì)打開門閂。

在講到"上門閂"這個(gè)功能的時(shí)候,我們提到了上門閂后線程就被掛起了,等待被喚醒。那么就看一下什么時(shí)候喚醒。

我們知道計(jì)數(shù)的減少是通過countDown這個(gè)方法來控制的,它對計(jì)數(shù)這個(gè)值是很敏感的,因?yàn)槊空{(diào)一次它會(huì)獲取這個(gè)計(jì)數(shù)進(jìn)行減1,也就是說當(dāng)計(jì)數(shù)變?yōu)?的時(shí)候是它觸發(fā)的,那么這個(gè)時(shí)候它很適合喚醒上門閂的線程。研究代碼發(fā)現(xiàn),的確如此,下面是從代碼的角度對這段邏輯的分析。

public void countDown() {
    sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
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;
    }
}

tryReleaseShared的代碼中我們可以看到nextc表示計(jì)數(shù)減1后的值,如果計(jì)數(shù)減1后為0則方法會(huì)返回true。這個(gè)返回結(jié)果為ture之后,releaseShared的下面這段方法就會(huì)執(zhí)行:

 doReleaseShared();

這段代碼我們不深究(因?yàn)樯婕暗紸QS的知識(shí)),我們只要知道它的功能就是喚醒上門閂的那個(gè)線程。

現(xiàn)在我們可以這樣總結(jié)了:也就是說不斷調(diào)用countDown方法,等到計(jì)數(shù)總數(shù)變成0之后 ,上門閂的那個(gè)線程就被喚醒了,門閂就被打開了,就可以繼續(xù)執(zhí)行門閂后面的代碼。

現(xiàn)在通過下面的場景來看看CountDownLatch的使用。

學(xué)生春游場景

場景描述:現(xiàn)在你們班要去春游,準(zhǔn)備做大巴車去,只有等所有的同學(xué)都上車之后,司機(jī)才會(huì)開車出發(fā)。

老師拿了個(gè)包含50個(gè)同學(xué)名字的名單,同學(xué)來一個(gè)就劃掉一個(gè),當(dāng)所有的同學(xué)都被劃掉后,說明所有的同學(xué)都到了,這時(shí)候就可以出發(fā)了。

import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.CountDownLatch;

public class SpringOuting {

    public static void main(String[] args) throws Exception {

        CountDownLatch cd = new CountDownLatch(50);// 學(xué)生名單
        
        // 司機(jī)
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    cd.await();// 等待所有的學(xué)生從名單中被劃掉
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                System.out.println("司機(jī)啟動(dòng)車出發(fā)....");
            }
        }, "司機(jī)").start();

        // 學(xué)生們
        Set<Thread> hashSet = new HashSet<>();
        for (int i=1; i<=50; i++) {
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + "上車了...");
                    cd.countDown();// 從名單中劃掉
                }
            }, "同學(xué)" + i);

           hashSet.add(t);
        }

        Iterator<Thread> it = hashSet.iterator();
        while (it.hasNext()) {
            Thread t = it.next();
            t.start();
            Thread.sleep(1000);
        }

    }

}

這里說個(gè)題外話,是關(guān)于sleep的,寫這個(gè)類的時(shí)候我想模擬同學(xué)一個(gè)一個(gè)上車的效果,當(dāng)時(shí)并不是在代碼最后一行這里加上sleep語句的,而是在springOuting.getOn()這個(gè)的上面加上sleep,測試發(fā)現(xiàn)沒有效果。經(jīng)過分析得到了原因:迭代這里啟動(dòng)線程,50個(gè)線程可以說是一瞬間啟動(dòng)了,雖然CPU每個(gè)時(shí)刻只有一個(gè)線程占用(假設(shè)單核),但是它切換線程足夠得快,使得這50個(gè)線程幾乎同一時(shí)間執(zhí)行到sleep代碼,這樣50個(gè)線程幾乎同時(shí)都休眠了,然后幾乎同一時(shí)間休眠結(jié)束,所以就把sleep加到表示同學(xué)的線程的內(nèi)部是不起作用的。

總結(jié)

最后通過JDK API的描述來說明這個(gè)類的功能:

CountDownLatch這個(gè)類能夠使一個(gè)線程等待其他線程完成各自的工作后再執(zhí)行。

應(yīng)用場景

此為《Java并發(fā)編程的藝術(shù)》提到的一個(gè)場景:我們需要解析一個(gè)Excel里多個(gè)sheet的數(shù)據(jù),此時(shí)可以考慮使用多線程,每個(gè)線程解析一個(gè)sheet里的數(shù)據(jù),等到所有的sheet都解析完之后,程序需要提示解析完成。在這個(gè)需求中,要實(shí)現(xiàn)主線程等待所有線程完成sheet的解析操作。

實(shí)現(xiàn)原理

具體原理參考:Java并發(fā)編程 - 共享鎖

簡要說明:線程執(zhí)行await判斷內(nèi)部令牌數(shù)是否為0,如果不為0,當(dāng)前線程就會(huì)被放入同步隊(duì)列中;其他線程執(zhí)行完自己的后調(diào)用countDown釋放1個(gè)令牌,當(dāng)最后一個(gè)調(diào)用的線程通過countDown把令牌數(shù)降至為0后,執(zhí)行同步隊(duì)列中的線程喚醒操作喚醒線程(多個(gè)線程await,由于喚醒的傳播性,則都會(huì)被喚醒)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 進(jìn)程和線程 進(jìn)程 所有運(yùn)行中的任務(wù)通常對應(yīng)一個(gè)進(jìn)程,當(dāng)一個(gè)程序進(jìn)入內(nèi)存運(yùn)行時(shí),即變成一個(gè)進(jìn)程.進(jìn)程是處于運(yùn)行過程中...
    勝浩_ae28閱讀 5,257評論 0 23
  • 進(jìn)程和線程 進(jìn)程 所有運(yùn)行中的任務(wù)通常對應(yīng)一個(gè)進(jìn)程,當(dāng)一個(gè)程序進(jìn)入內(nèi)存運(yùn)行時(shí),即變成一個(gè)進(jìn)程.進(jìn)程是處于運(yùn)行過程中...
    小徐andorid閱讀 2,993評論 3 53
  • 線程池ThreadPoolExecutor corepoolsize:核心池的大小,默認(rèn)情況下,在創(chuàng)建了線程池之后...
    irckwk1閱讀 864評論 0 0
  • 在2014年6月辭職以后,我決心換一種生活,我來到一個(gè)房地產(chǎn)公司做采購員,開始了我的職業(yè)生涯。剛開始來的時(shí)候我在辦...
    丁二烯861閱讀 315評論 5 2
  • 文/雨夜梅子 (本文純屬虛構(gòu),如有雷同純屬巧合) (一) 漫天雪花飛舞,散落一地。雪越積越厚,瞬間淹沒了干涸的生命...
    雨夜梅子閱讀 1,724評論 7 49

友情鏈接更多精彩內(nèi)容