CountDownLatch和CyclicBarrier介紹

概述

JDK中提供了一些用于線程之間協同等待的工具類,CountDownLatchCyclicBarrier就是最典型的兩個線程同步輔助類。下面分別詳細介紹這兩個類,以及他們之間的異同點。

CountDownLatch類

CountDownLatch顧名思義:倒計數鎖存器。沒錯,他就是一個計數器,并且是倒著計數的。他的應用場景如下:

一個任務A,他需要等待其他的一些任務都執行完畢之后它才能執行。就比如說賽跑的時候,發令員需要等待所有運動員都準備好了才能發令,否則不被K才怪嘞!

此時CountDownLatch就可以大展身手了。

常用操作

本節介紹CountDownLatch的基本操作函數。

構造函數

函數簽名如下:

public CountDownLatch(int count)

用一個給定的數值初始化CountDownLatch,之后計數器就從這個值開始倒計數,直到計數值達到零。

await函數

await函數用兩種形式,簽名分別如下:

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

這兩個函數的作用都是讓線程阻塞等待其他線程,直到CountDownLatch的計數值變為0才繼續執行之后的操作。區別在于第一個函數沒有等待時間限制,可以等到天荒地老,海枯石爛,第二個函數給定一個等待超時時間,超過該時間就直接放棄了,并且第二個函數具有返回值,超時時間之內CountDownLatch的值達到0就返回true,等待時間結束計數值都還沒達到0就返回false。這兩個操作在等待過程中如果等待的線程被中斷,則會拋出InterruptedException異常。

countDown函數

這個函數用來將CountDownLatch的計數值減一,函數簽名如下:

public void countDown()

需要說明的是,如果調用這個函數的時候CountDownLatch的計數值已經為0,那么這個函數什么也不會做。

getCount函數

該函數用來獲取當前CountDownLatch的計數值,函數簽名如下:

public void countDown()

模擬實驗

理論知識講完了,需要真槍實戰地來演示一下這個類的作用,我們就以下面這個場景為例子,用CountDownLatch來實現這個需求:

有5個運動員賽跑,開跑之前,裁判需要等待5個運動員都準備好才能發令,并且5個運動員準備好之后也都需要等待裁判發令才能開跑。

首先分析一下依賴關系:

裁判發令 -> 5個運動員都準備好;
5個運動員開跑 -> 裁判發令。

好,依賴關系已經出來了,代碼實現:

package com.winwill.test;

import java.util.Random;
import java.util.concurrent.CountDownLatch;

/**
 * @author qifuguang
 * @date 15/8/24 23:35
 */
public class TestCountDownLatch {
    private static final int RUNNER_NUMBER = 5; // 運動員個數
    private static final Random RANDOM = new Random();

    public static void main(String[] args) {
        // 用于判斷發令之前運動員是否已經完全進入準備狀態,需要等待5個運動員,所以參數為5
        CountDownLatch readyLatch = new CountDownLatch(RUNNER_NUMBER);
        // 用于判斷裁判是否已經發令,只需要等待一個裁判,所以參數為1
        CountDownLatch startLatch = new CountDownLatch(1);
        for (int i = 0; i < RUNNER_NUMBER; i++) {
            Thread t = new Thread(new Runner((i + 1) + "號運動員", readyLatch, startLatch));
            t.start();
        }
        try {
            readyLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        startLatch.countDown();
        System.out.println("裁判:所有運動員準備完畢,開始...");
    }

    static class Runner implements Runnable {
        private CountDownLatch readyLatch;
        private CountDownLatch startLatch;
        private String name;

        public Runner(String name, CountDownLatch readyLatch, CountDownLatch startLatch) {
            this.name = name;
            this.readyLatch = readyLatch;
            this.startLatch = startLatch;
        }

        public void run() {
            int readyTime = RANDOM.nextInt(1000);
            System.out.println(name + ":我需要" + readyTime + "秒時間準備.");
            try {
                Thread.sleep(readyTime);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(name + ":我已經準備完畢.");
            readyLatch.countDown();
            try {
                startLatch.await();  // 等待裁判發開始命令
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(name + ":開跑...");
        }
    }
}

運行結果如下:

1號運動員:我需要389秒時間準備.
2號運動員:我需要449秒時間準備.
3號運動員:我需要160秒時間準備.
4號運動員:我需要325秒時間準備.
5號運動員:我需要978秒時間準備.
3號運動員:我已經準備完畢.
4號運動員:我已經準備完畢.
1號運動員:我已經準備完畢.
2號運動員:我已經準備完畢.
5號運動員:我已經準備完畢.
裁判:所有運動員準備完畢,開始...
1號運動員:開跑...
5號運動員:開跑...
2號運動員:開跑...
4號運動員:開跑...
3號運動員:開跑...

可以看到,一切都是如此地完美,運動員準備好了之后裁判才發令,裁判發令之后運動員才開跑。

CyclicBarrier類

CyclicBarrier翻譯過來就是:循環的屏障。什么是循環?可以重復利用唄,對這個類就是一個可以重復利用的屏障類。CyclicBarrier主要用于一組固定大小的線程之間,各個線程之間相互等待,當所有線程都完成某項任務之后,才能執行之后的任務。
如下場景:

有若干個線程都需要向一個數據庫寫數據,但是必須要所有的線程都講數據寫入完畢他們才能繼續做之后的事情。

分析一下這個場景的特征:

  • 各個線程都必須完成某項任務(寫數據)才能繼續做后續的任務;
  • 各個線程需要相互等待,不能獨善其身。

這種場景便可以利用CyclicBarrier來完美解決。

常用函數

本節介紹CyclicBarrier的基本操作函數。

構造函數

有兩種類型的構造函數,函數簽名分別如下:

public CyclicBarrier(int parties, Runnable barrierAction)
public CyclicBarrier(int parties)

參數parties表示一共有多少線程參與這次“活動”,參數barrierAction是可選的,用來指定當所有線程都完成這些必須的“神秘任務”之后需要干的事情,所以barrierAction這里的動作在一個相互等待的循環內只會執行一次。

getParties函數

getParties用來獲取當前的CyclicBarrier一共有多少線程參數與,函數簽名如下:

public int getParties()

返回參與“活動”的線程個數。

await函數

await函數用來執行等待操作,有兩種類型的函數簽名:

public int await() throws InterruptedException, BrokenBarrierException
public int await(long timeout, TimeUnit unit)
        throws InterruptedException,
               BrokenBarrierException,
               TimeoutException 

第一個函數是一個無參函數,第二個函數可以指定等待的超時時間。它們的作用是:一直等待知道所有參與“活動”的線程都調用過await函數,如果當前線程不是即將調用await函數的的最后一個線程,當前線程將會被掛起,直到下列某一種情況發生:

  • 最后一個線程調用了await函數;
  • 某個線程打斷了當前線程;
  • 某個線程打斷了其他某個正在等待的線程;
  • 其他某個線程等待時間超過給定的超時時間;
  • 其他某個線程調用了reset函數。

如果等待過程中線程被打斷了,則會拋出InterruptedException異常;
如果等待過程中出現下列情況中的某一種情況,則會拋出BrokenBarrierException異常:

  • 其他線程被打斷了;
  • 當前線程等待超時了;
  • 當前CyclicBarrier被reset了;
  • 等待過程中CyclicBarrier損壞了;
  • 構造函數中指定的barrierAction在執行過程中發生了異常。

如果等待時間超過給定的最大等待時間,則會拋出TimeoutException異常,并且這個時候其他已經嗲用過await函數的線程將會繼續后續的動作。

返回值:返回當前線程在調用過await函數的所以線程中的編號,編號為parties-1的表示第一個調用await函數,編號為0表示是最后一個調用await函數。

isBroken函數

給函數用來判斷barrier是否已經損壞,函數簽名如下:

public boolean isBroken()

如果因為任何原因被損壞返回true,否則返回false

reset函數

顧名思義,這個函數用來重置barrier,函數簽名如下:

public void reset()

如果調用了該函數,則在等待的線程將會拋出BrokenBarrierException異常。

getNumberWaiting函數

該函數用來獲取當前正在等待該barrier的線程數,函數簽名如下:

public int getNumberWaiting()

模擬實驗

下面用代碼實現下面的場景:

有5個線程都需要向一個數據庫寫數據,但是必須要所有的線程都講數據寫入完畢他們才能繼續做之后的事情。

一般情況

代碼:

package com.winwill.test;

import java.util.Random;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

/**
 * @author qifuguang
 * @date 15/8/25 00:34
 */
public class TestCyclicBarrier {
    private static final int THREAD_NUMBER = 5;
    private static final Random RANDOM = new Random();

    public static void main(String[] args) {
        CyclicBarrier barrier = new CyclicBarrier(THREAD_NUMBER, new Runnable() {
            public void run() {
                System.out.println(Thread.currentThread().getId() + ":我宣布,所有小伙伴寫入數據完畢");
            }
        });
        for (int i = 0; i < THREAD_NUMBER; i++) {
            Thread t = new Thread(new Worker(barrier));
            t.start();
        }
    }

    static class Worker implements Runnable {
        private CyclicBarrier barrier;

        public Worker(CyclicBarrier barrier) {
            this.barrier = barrier;
        }

        public void run() {
            int time = RANDOM.nextInt(1000);
            System.out.println(Thread.currentThread().getId() + ":我需要" + time + "毫秒時間寫入數據.");
            try {
                Thread.sleep(time);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getId() + ":寫入數據完畢,等待其他小伙伴...");
            try {
                barrier.await(); // 等待所有線程都調用過此函數才能進行后續動作
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getId() + ":所有線程都寫入數據完畢,繼續干活...");
        }
    }
}

運行結果如下:

10:我需要16毫秒時間寫入數據.
11:我需要353毫秒時間寫入數據.
12:我需要101毫秒時間寫入數據.
13:我需要744毫秒時間寫入數據.
14:我需要51毫秒時間寫入數據.
10:寫入數據完畢,等待其他小伙伴...
14:寫入數據完畢,等待其他小伙伴...
12:寫入數據完畢,等待其他小伙伴...
11:寫入數據完畢,等待其他小伙伴...
13:寫入數據完畢,等待其他小伙伴...
13:我宣布,所有小伙伴寫入數據完畢
13:所有線程都寫入數據完畢,繼續干活...
10:所有線程都寫入數據完畢,繼續干活...
12:所有線程都寫入數據完畢,繼續干活...
14:所有線程都寫入數據完畢,繼續干活...
11:所有線程都寫入數據完畢,繼續干活...

可以看到,線程小伙伴們非常團結,寫完自己的數據之后都在等待其他小伙伴,所有小伙伴都完成之后才繼續后續的動作。

重復使用

上面的例子并沒有體現CyclicBarrier可以循環使用的特點,所以有如下代碼:

package com.winwill.test;

import java.util.Random;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

/**
 * @author qifuguang
 * @date 15/8/25 00:34
 */
public class TestCyclicBarrier {
    private static final int THREAD_NUMBER = 5;
    private static final Random RANDOM = new Random();

    public static void main(String[] args) throws Exception {
        CyclicBarrier barrier = new CyclicBarrier(THREAD_NUMBER, new Runnable() {
            public void run() {
                System.out.println(Thread.currentThread().getId() + ":我宣布,所有小伙伴寫入數據完畢");
            }
        });
        for (int i = 0; i < THREAD_NUMBER; i++) {
            Thread t = new Thread(new Worker(barrier));
            t.start();
        }
        Thread.sleep(10000);
        System.out.println("================barrier重用==========================");
        for (int i = 0; i < THREAD_NUMBER; i++) {
            Thread t = new Thread(new Worker(barrier));
            t.start();
        }
    }

    static class Worker implements Runnable {
        private CyclicBarrier barrier;

        public Worker(CyclicBarrier barrier) {
            this.barrier = barrier;
        }

        public void run() {
            int time = RANDOM.nextInt(1000);
            System.out.println(Thread.currentThread().getId() + ":我需要" + time + "毫秒時間寫入數據.");
            try {
                Thread.sleep(time);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getId() + ":寫入數據完畢,等待其他小伙伴...");
            try {
                barrier.await(); // 等待所有線程都調用過此函數才能進行后續動作
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getId() + ":所有線程都寫入數據完畢,繼續干活...");
        }
    }
}

運行結果:

10:我需要228毫秒時間寫入數據.
11:我需要312毫秒時間寫入數據.
12:我需要521毫秒時間寫入數據.
13:我需要720毫秒時間寫入數據.
14:我需要377毫秒時間寫入數據.
10:寫入數據完畢,等待其他小伙伴...
11:寫入數據完畢,等待其他小伙伴...
14:寫入數據完畢,等待其他小伙伴...
12:寫入數據完畢,等待其他小伙伴...
13:寫入數據完畢,等待其他小伙伴...
13:我宣布,所有小伙伴寫入數據完畢
13:所有線程都寫入數據完畢,繼續干活...
10:所有線程都寫入數據完畢,繼續干活...
11:所有線程都寫入數據完畢,繼續干活...
14:所有線程都寫入數據完畢,繼續干活...
12:所有線程都寫入數據完畢,繼續干活...
================barrier重用==========================
15:我需要212毫秒時間寫入數據.
16:我需要691毫秒時間寫入數據.
17:我需要530毫秒時間寫入數據.
18:我需要758毫秒時間寫入數據.
19:我需要604毫秒時間寫入數據.
15:寫入數據完畢,等待其他小伙伴...
17:寫入數據完畢,等待其他小伙伴...
19:寫入數據完畢,等待其他小伙伴...
16:寫入數據完畢,等待其他小伙伴...
18:寫入數據完畢,等待其他小伙伴...
18:我宣布,所有小伙伴寫入數據完畢
18:所有線程都寫入數據完畢,繼續干活...
15:所有線程都寫入數據完畢,繼續干活...
19:所有線程都寫入數據完畢,繼續干活...
16:所有線程都寫入數據完畢,繼續干活...
17:所有線程都寫入數據完畢,繼續干活...

可以看到,barrier的確是重用了。

等待超時

如果await的時候設置了一個最長等待時間,并且等待超時,則會怎么樣呢?下面的例子故意讓一個線程延遲一段時間才開始寫數據,這樣就會出現先等待的線程等待最后一個線程拋出等待超時異常的情況。

package com.winwill.test;

import java.util.Random;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * @author qifuguang
 * @date 15/8/25 00:34
 */
public class TestCyclicBarrier {
    private static final int THREAD_NUMBER = 5;
    private static final Random RANDOM = new Random();

    public static void main(String[] args) throws Exception {
        CyclicBarrier barrier = new CyclicBarrier(THREAD_NUMBER, new Runnable() {
            public void run() {
                System.out.println(Thread.currentThread().getId() + ":我宣布,所有小伙伴寫入數據完畢");
            }
        });
        for (int i = 0; i < THREAD_NUMBER; i++) {
            if (i < THREAD_NUMBER - 1) {
                Thread t = new Thread(new Worker(barrier));
                t.start();
            } else {  //最后一個線程故意延遲3s創建。
                Thread.sleep(3000);
                Thread t = new Thread(new Worker(barrier));
                t.start();
            }
        }
    }

    static class Worker implements Runnable {
        private CyclicBarrier barrier;

        public Worker(CyclicBarrier barrier) {
            this.barrier = barrier;
        }

        public void run() {
            int time = RANDOM.nextInt(1000);
            System.out.println(Thread.currentThread().getId() + ":我需要" + time + "毫秒時間寫入數據.");
            try {
                Thread.sleep(time);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getId() + ":寫入數據完畢,等待其他小伙伴...");
            try {
                barrier.await(2000, TimeUnit.MILLISECONDS); // 只等待2s,必然會等待最后一個線程超時
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            } catch (TimeoutException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getId() + ":所有線程都寫入數據完畢,繼續干活...");
        }
    }
}

運行結果:

10:我需要820毫秒時間寫入數據.
11:我需要140毫秒時間寫入數據.
12:我需要640毫秒時間寫入數據.
13:我需要460毫秒時間寫入數據.
11:寫入數據完畢,等待其他小伙伴...
13:寫入數據完畢,等待其他小伙伴...
12:寫入數據完畢,等待其他小伙伴...
10:寫入數據完畢,等待其他小伙伴...
java.util.concurrent.BrokenBarrierException
12:所有線程都寫入數據完畢,繼續干活...
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
13:所有線程都寫入數據完畢,繼續干活...
11:所有線程都寫入數據完畢,繼續干活...
10:所有線程都寫入數據完畢,繼續干活...
at com.winwill.test.TestCyclicBarrier$Worker.run(TestCyclicBarrier.java:52)
at java.lang.Thread.run(Thread.java:744)
java.util.concurrent.BrokenBarrierException
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
at com.winwill.test.TestCyclicBarrier$Worker.run(TestCyclicBarrier.java:52)
at java.lang.Thread.run(Thread.java:744)
java.util.concurrent.TimeoutException
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:257)
at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
at com.winwill.test.TestCyclicBarrier$Worker.run(TestCyclicBarrier.java:52)
at java.lang.Thread.run(Thread.java:744)
java.util.concurrent.BrokenBarrierException
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:250)
at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
at com.winwill.test.TestCyclicBarrier$Worker.run(TestCyclicBarrier.java:52)
at java.lang.Thread.run(Thread.java:744)
14:我需要850毫秒時間寫入數據.
java.util.concurrent.BrokenBarrierException
14:寫入數據完畢,等待其他小伙伴...
14:所有線程都寫入數據完畢,繼續干活...
at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:207)
at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:435)
at com.winwill.test.TestCyclicBarrier$Worker.run(TestCyclicBarrier.java:52)
at java.lang.Thread.run(Thread.java:744)

可以看到,前面四個線程等待最后一個線程超時了,這個時候他們不再等待最后這個小伙伴了,而是拋出異常并都繼續后續的動作。最后這個線程屁顛屁顛地完成寫入數據操作之后也繼續了后續的動作。需要說明的是,發生了超時異常時候,還沒有完成“神秘任務”的線程在完成任務之后不會做任何等待,而是會直接執行后續的操作。

總結

CountDownLatch和CyclicBarrier都能夠實現線程之間的等待,只不過它們側重點不同:

  • CountDownLatch一般用于某個線程A等待若干個其他線程執行完任務之后,它才執行;
  • CyclicBarrier一般用于一組線程互相等待至某個狀態,然后這一組線程再同時執行;
  • CountDownLatch是不能夠重用的,而CyclicBarrier是可以重用的。

聲明

本文為作者原創,也純屬個人見解,如理解有誤,請留言相告。轉載請注明出處: http://qifuguang.me/2015/08/25/[Java并發包學習五]CountDownLatch和CyclicBarrier介紹
如果你喜歡我的文章,請關注我的微信訂閱號:“機智的程序猿”,更多精彩,盡在其中:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 一、多線程 說明下線程的狀態 java中的線程一共有 5 種狀態。 NEW:這種情況指的是,通過 New 關鍵字創...
    Java旅行者閱讀 4,721評論 0 44
  • Java-Review-Note——4.多線程 標簽: JavaStudy PS:本來是分開三篇的,后來想想還是整...
    coder_pig閱讀 1,671評論 2 17
  • 相關概念 面向對象的三個特征 封裝,繼承,多態.這個應該是人人皆知.有時候也會加上抽象. 多態的好處 允許不同類對...
    東經315度閱讀 1,976評論 0 8
  • layout: posttitle: 《Java并發編程的藝術》筆記categories: Javaexcerpt...
    xiaogmail閱讀 5,858評論 1 19
  • 星期四/晴 近日陽光大好,無限春光將長清慣有的妖風也融化了,卸去八分凜冽的微風拂在面頰上,教人好生歡喜。 早上出門...
    酒久里個丸子閱讀 129評論 0 0