Java并發(fā)工具類--CountDownLatch詳解

CountDownLatch是一個(gè)同步工具類,用來協(xié)調(diào)多個(gè)線程之間的同步。

CountDownLatch能夠使一個(gè)線程在等待另外一些線程完成各自工作之后,再繼續(xù)執(zhí)行。使用一個(gè)計(jì)數(shù)器進(jìn)行實(shí)現(xiàn)冗懦。計(jì)數(shù)器初始值為線程的數(shù)量。當(dāng)每一個(gè)線程完成自己任務(wù)后仇祭,計(jì)數(shù)器的值就會(huì)減一披蕉。當(dāng)計(jì)數(shù)器的值為0時(shí),表示所有的線程都已經(jīng)完成一些任務(wù)乌奇,然后在CountDownLatch上等待的線程就可以恢復(fù)執(zhí)行接下來的任務(wù)没讲。

舉個(gè)例子來說明CountDownLatch的使用:

百米賽跑,10名運(yùn)動(dòng)員選手到達(dá)場地等待裁判口令礁苗,裁判一聲口令爬凑,選手聽到后同時(shí)起跑,當(dāng)所有選手到達(dá)終點(diǎn)试伙,裁判進(jìn)行匯總排名嘁信。

public class CountDownLatchTest {

    public static void main(String[] args) {
        ExecutorService service = Executors.newCachedThreadPool();
        final CountDownLatch cdOrder = new CountDownLatch(1);
        final CountDownLatch cdAnswer = new CountDownLatch(10);
        for(int i=0; i<10; i++){
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    try{
                        System.out.println("選手" + Thread.currentThread().getName() + "正在等待裁判發(fā)布口令");
                        cdOrder.await();
                        System.out.println("選手" + Thread.currentThread().getName() + "已接受裁判口令");
                        Thread.sleep((long) (Math.random() * 10000));
                        System.out.println("選手" + Thread.currentThread().getName() + "到達(dá)終點(diǎn)");
                        cdAnswer.countDown();
                    }catch (InterruptedException ie){
                        ie.printStackTrace();
                    }
                }
            };
            service.execute(runnable);
        }

        try {
            Thread.sleep((long) (Math.random() * 10000));
            System.out.println("裁判"+Thread.currentThread().getName()+"即將發(fā)布口令");
            cdOrder.countDown();
            System.out.println("裁判"+Thread.currentThread().getName()+"已發(fā)送口令,正在等待所有選手到達(dá)終點(diǎn)");
            cdAnswer.await();
            System.out.println("所有選手都到達(dá)終點(diǎn)");
            System.out.println("裁判"+Thread.currentThread().getName()+"匯總成績排名");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        service.shutdown();
    }
}

運(yùn)行結(jié)果如下:

選手pool-1-thread-1正在等待裁判發(fā)布口令
選手pool-1-thread-3正在等待裁判發(fā)布口令
選手pool-1-thread-2正在等待裁判發(fā)布口令
選手pool-1-thread-4正在等待裁判發(fā)布口令
選手pool-1-thread-5正在等待裁判發(fā)布口令
選手pool-1-thread-6正在等待裁判發(fā)布口令
選手pool-1-thread-7正在等待裁判發(fā)布口令
選手pool-1-thread-8正在等待裁判發(fā)布口令
選手pool-1-thread-9正在等待裁判發(fā)布口令
選手pool-1-thread-10正在等待裁判發(fā)布口令
裁判main即將發(fā)布口令
裁判main已發(fā)送口令疏叨,正在等待所有選手到達(dá)終點(diǎn)
選手pool-1-thread-3已接受裁判口令
選手pool-1-thread-2已接受裁判口令
選手pool-1-thread-1已接受裁判口令
選手pool-1-thread-9已接受裁判口令
選手pool-1-thread-8已接受裁判口令
選手pool-1-thread-7已接受裁判口令
選手pool-1-thread-6已接受裁判口令
選手pool-1-thread-5已接受裁判口令
選手pool-1-thread-4已接受裁判口令
選手pool-1-thread-10已接受裁判口令
選手pool-1-thread-7到達(dá)終點(diǎn)
選手pool-1-thread-3到達(dá)終點(diǎn)
選手pool-1-thread-10到達(dá)終點(diǎn)
選手pool-1-thread-9到達(dá)終點(diǎn)
選手pool-1-thread-1到達(dá)終點(diǎn)
選手pool-1-thread-5到達(dá)終點(diǎn)
選手pool-1-thread-4到達(dá)終點(diǎn)
選手pool-1-thread-2到達(dá)終點(diǎn)
選手pool-1-thread-8到達(dá)終點(diǎn)
選手pool-1-thread-6到達(dá)終點(diǎn)
所有選手都到達(dá)終點(diǎn)
裁判main匯總成績排名

具體看一下CountDownLatch是如何實(shí)現(xiàn)線程調(diào)度的潘靖。

首先看一下其構(gòu)造方法:

public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}

繼續(xù)看一下Sync。

// Sync繼承自AQS
private static final class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = 4982264981922014374L;

    Sync(int count) {
        setState(count);
    }

    int getCount() {
        return getState();
    }
    // 定義嘗試獲取共享鎖的方法
    protected int tryAcquireShared(int acquires) {
        // 當(dāng)狀態(tài)為0蚤蔓,則該線程獲取到該共享鎖
        return (getState() == 0) ? 1 : -1;
    }

    protected boolean tryReleaseShared(int releases) {
        // 減少count卦溢,當(dāng)檢測到狀態(tài)值為0時(shí),通知同步隊(duì)列中被掛起的線程
        for (;;) {
            int c = getState();
            if (c == 0)
                return false;
            int nextc = c-1;
            if (compareAndSetState(c, nextc))
                return nextc == 0;
        }
    }
}

可以發(fā)現(xiàn)秀又,CountDownLatch是基于AQS共享鎖來實(shí)現(xiàn)的单寂,,只要共享鎖狀態(tài)值不為0,則請求共享鎖的線程均會(huì)添加到同步隊(duì)列中涮坐,阻塞掛起凄贩,等待被通知。

接著看一下await方法:

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

直接調(diào)用的是AQS的acquireSharedInterruptibly方法:

public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // tryAcquireShared由Sync實(shí)現(xiàn)袱讹,即只要狀態(tài)不為0,則返回-1
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}

CountDownLatch初始化后狀態(tài)值肯定不為0,所以當(dāng)前線程tryAcquireShared必然返回-1捷雕,繼續(xù)執(zhí)行doAcquireSharedInterruptibly方法椒丧。

private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    // 將當(dāng)前線程添加到同步隊(duì)列
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            // 若當(dāng)前線程所在節(jié)點(diǎn)的前繼節(jié)點(diǎn)為頭節(jié)點(diǎn),則執(zhí)行tryAcquireShared嘗試獲取共享鎖
            if (p == head) {
                // 因?yàn)闋顟B(tài)值不為0救巷,tryAcquireShared必然返回-1
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            // 將前繼節(jié)點(diǎn)狀態(tài)CAS更改為SIGNAL后壶熏,然后線程阻塞掛起
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

綜上,持有CountDownLatch的線程只要調(diào)用await方法浦译,就會(huì)被添加進(jìn)AQS的同步隊(duì)列棒假,并被阻塞掛起。

那這些被阻塞掛起的線程啥時(shí)候會(huì)被喚醒繼續(xù)執(zhí)行呢精盅?

答案在CountDownLatch的countDown方法中:

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

底層調(diào)用的是AQS的releaseShared方法:

public final boolean releaseShared(int arg) {
    // 若狀態(tài)執(zhí)行-1操作后帽哑,狀態(tài)值未歸零,tryReleaseShared返回false
    // 若狀態(tài)執(zhí)行-1操作后叹俏,狀態(tài)值歸零妻枕,tryReleaseShared返回true
    if (tryReleaseShared(arg)) {
        // 若狀態(tài)值歸零,繼續(xù)執(zhí)行doReleaseShared方法
        doReleaseShared();
        return true;
    }
    return false;
}

當(dāng)狀態(tài)值歸零后粘驰,當(dāng)前線程會(huì)執(zhí)行AQS的doReleaseShared方法屡谐,doReleaseShared方法我們在之前AQS詳解的系列文章里詳細(xì)介紹過,該方法是一個(gè)"喚醒風(fēng)暴"蝌数,其會(huì)喚醒同步隊(duì)列中阻塞掛起的線程愕掏。

喚醒后的線程會(huì)進(jìn)行獲取鎖的操作,當(dāng)狀態(tài)值歸零后顶伞,由于tryReleaseShared恒返回1饵撑,代表任何線程均可以獲取共享鎖成功,當(dāng)線程獲取到鎖之后枝哄,會(huì)執(zhí)行setHeadAndPropagate方法將當(dāng)前節(jié)點(diǎn)置為頭節(jié)點(diǎn)并喚醒后繼節(jié)點(diǎn)肄梨,后繼節(jié)點(diǎn)被喚醒后執(zhí)行獲取鎖操作,如此反復(fù)挠锥,直到同步隊(duì)列中的所有阻塞線程被喚醒众羡。

需要注意的是:CountDownLatch是一次性的,計(jì)算器的值只能在構(gòu)造方法中初始化一次蓖租,之后沒有任何機(jī)制再次對其設(shè)置值粱侣,當(dāng)CountDownLatch使用完畢后,它不能再次被使用蓖宦。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末齐婴,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子稠茂,更是在濱河造成了極大的恐慌柠偶,老刑警劉巖情妖,帶你破解...
    沈念sama閱讀 221,198評論 6 514
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異诱担,居然都是意外死亡毡证,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,334評論 3 398
  • 文/潘曉璐 我一進(jìn)店門蔫仙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來料睛,“玉大人,你說我怎么就攤上這事摇邦⌒羯罚” “怎么了?”我有些...
    開封第一講書人閱讀 167,643評論 0 360
  • 文/不壞的土叔 我叫張陵施籍,是天一觀的道長居扒。 經(jīng)常有香客問我,道長法梯,這世上最難降的妖魔是什么苔货? 我笑而不...
    開封第一講書人閱讀 59,495評論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮立哑,結(jié)果婚禮上夜惭,老公的妹妹穿的比我還像新娘。我一直安慰自己铛绰,他們只是感情好诈茧,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,502評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著捂掰,像睡著了一般敢会。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上这嚣,一...
    開封第一講書人閱讀 52,156評論 1 308
  • 那天鸥昏,我揣著相機(jī)與錄音,去河邊找鬼姐帚。 笑死吏垮,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的罐旗。 我是一名探鬼主播膳汪,決...
    沈念sama閱讀 40,743評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼九秀!你這毒婦竟也來了遗嗽?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,659評論 0 276
  • 序言:老撾萬榮一對情侶失蹤鼓蜒,失蹤者是張志新(化名)和其女友劉穎痹换,沒想到半個(gè)月后征字,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,200評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡晴音,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,282評論 3 340
  • 正文 我和宋清朗相戀三年柔纵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了缔杉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片锤躁。...
    茶點(diǎn)故事閱讀 40,424評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖或详,靈堂內(nèi)的尸體忽然破棺而出系羞,到底是詐尸還是另有隱情,我是刑警寧澤霸琴,帶...
    沈念sama閱讀 36,107評論 5 349
  • 正文 年R本政府宣布椒振,位于F島的核電站,受9級特大地震影響梧乘,放射性物質(zhì)發(fā)生泄漏澎迎。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,789評論 3 333
  • 文/蒙蒙 一选调、第九天 我趴在偏房一處隱蔽的房頂上張望夹供。 院中可真熱鬧,春花似錦仁堪、人聲如沸哮洽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,264評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鸟辅。三九已至,卻和暖如春莺葫,著一層夾襖步出監(jiān)牢的瞬間匪凉,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,390評論 1 271
  • 我被黑心中介騙來泰國打工捺檬, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留再层,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,798評論 3 376
  • 正文 我出身青樓欺冀,卻偏偏與公主長得像树绩,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子隐轩,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,435評論 2 359