參考:https://blog.csdn.net/zhutulang/article/details/48504487
概念:
CountDownLatch是通過一個計數器來實現的,計數器的初始值爲線程的數量。每當一個線程完成了自己的任務後,計數器的值就會減1。當計數器值到達0時,它表示所有的線程已經完成了任務,然後在閉鎖上等待的線程就可以恢復執行任務。
構造器中的計數值(count)實際上就是閉鎖需要等待的線程數量。這個值只能被設置一次,而且CountDownLatch沒有提供任何機制去重新設置這個計數值。
與CountDownLatch的第一次交互是主線程等待其他線程。主線程必須在啓動其他線程後立即調CountDownLatch.await()方法。這樣主線程的操作就會在這個方法上阻塞,直到其他線程完成各自的任務。
其他N個線程必須引用閉鎖對象,因爲他們需要通知CountDownLatch對象,他們已經完成了各自的任務。這種通知機制是通過 CountDownLatch.countDown()方法來完成的;每調用一次這個方法,在構造函數中初始化的count值就減1。所以當N個線程都調 用了這個方法,count的值等於0,然後主線程就能通過await()方法,恢復執行自己的任務。
使用場景:
首先,我們來看一個應用場景1:
假設一條流水線上有三個工作者:worker0,worker1,worker2。有一個任務的完成需要他們三者協作完成,worker2可以開始這個任務的前提是worker0和worker1完成了他們的工作,而worker0和worker1是可以並行他們各自的工作的。
如果我們要編碼模擬上面的場景的話,我們大概很容易就會想到可以用join來做。當在當前線程中調用某個線程 thread 的 join() 方法時,當前線程就會阻塞,直到thread 執行完成,當前線程纔可以繼續往下執行。補充下:join的工作原理是,不停檢查thread是否存活,如果存活則讓當前線程永遠wait,直到thread線程終止,線程的this.notifyAll 就會被調用。
我們首先用join來模擬這個場景:
Worker類如下:
- package com.concurrent.test3;
- /**
- * 工作者類
- * @author ThinkPad
- *
- */
- public class Worker extends Thread {
- //工作者名
- private String name;
- //工作時間
- private long time;
- public Worker(String name, long time) {
- this.name = name;
- this.time = time;
- }
- @Override
- public void run() {
- // TODO 自動生成的方法存根
- try {
- System.out.println(name+"開始工作");
- Thread.sleep(time);
- System.out.println(name+"工作完成,耗費時間="+time);
- } catch (InterruptedException e) {
- // TODO 自動生成的 catch 塊
- e.printStackTrace();
- }
- }
- }
- package com.concurrent.test3;
- public class Test {
- public static void main(String[] args) throws InterruptedException {
- // TODO 自動生成的方法存根
- Worker worker0 = new Worker("worker0", (long) (Math.random()*2000+3000));
- Worker worker1 = new Worker("worker1", (long) (Math.random()*2000+3000));
- Worker worker2 = new Worker("worker2", (long) (Math.random()*2000+3000));
- worker0.start();
- worker1.start();
- worker0.join();
- worker1.join();
- System.out.println("準備工作就緒");
- worker2.start();
- }
- }
worker1開始工作
worker0開始工作
worker1工作完成,耗費時間=3947
worker0工作完成,耗費時間=4738
準備工作就緒
worker2開始工作
worker2工作完成,耗費時間=4513
除了用join外,用CountDownLatch 也可以完成這個需求。需要對worker做一點修改,我把它放在另一個包下:
Worker:
- package com.concurrent.test4;
- import java.util.concurrent.CountDownLatch;
- /**
- * 工作者類
- * @author ThinkPad
- *
- */
- public class Worker extends Thread {
- //工作者名
- private String name;
- //工作時間
- private long time;
- private CountDownLatch countDownLatch;
- public Worker(String name, long time, CountDownLatch countDownLatch) {
- this.name = name;
- this.time = time;
- this.countDownLatch = countDownLatch;
- }
- @Override
- public void run() {
- // TODO 自動生成的方法存根
- try {
- System.out.println(name+"開始工作");
- Thread.sleep(time);
- System.out.println(name+"工作完成,耗費時間="+time);
- countDownLatch.countDown();
- System.out.println("countDownLatch.getCount()="+countDownLatch.getCount());
- } catch (InterruptedException e) {
- // TODO 自動生成的 catch 塊
- e.printStackTrace();
- }
- }
- }
- package com.concurrent.test4;
- import java.util.concurrent.CountDownLatch;
- public class Test {
- public static void main(String[] args) throws InterruptedException {
- // TODO 自動生成的方法存根
- CountDownLatch countDownLatch = new CountDownLatch(2);
- Worker worker0 = new Worker("worker0", (long) (Math.random()*2000+3000), countDownLatch);
- Worker worker1 = new Worker("worker1", (long) (Math.random()*2000+3000), countDownLatch);
- Worker worker2 = new Worker("worker2", (long) (Math.random()*2000+3000), countDownLatch);
- worker0.start();
- worker1.start();
- countDownLatch.await();
- System.out.println("準備工作就緒");
- worker2.start();
- }
- }
我們創建了一個計數器爲2的 CountDownLatch ,讓Worker持有這個CountDownLatch 實例,當完成自己的工作後,調用countDownLatch.countDown() 方法將計數器減1。countDownLatch.await() 方法會一直阻塞直到計數器爲0,主線程纔會繼續往下執行。觀察運行結果,發現這樣也是可以的:
worker1開始工作
worker0開始工作
worker0工作完成,耗費時間=3174
countDownLatch.getCount()=1
worker1工作完成,耗費時間=3870
countDownLatch.getCount()=0
準備工作就緒
worker2開始工作
worker2工作完成,耗費時間=3992
countDownLatch.getCount()=0
那麼既然如此,CountDownLatch與join的區別在哪裏呢?事實上在這裏我們只要考慮另一種場景,就可以很清楚地看到它們的不同了。
應用場景2:
假設worker的工作可以分爲兩個階段,work2 只需要等待work0和work1完成他們各自工作的第一個階段之後就可以開始自己的工作了,而不是場景1中的必須等待work0和work1把他們的工作全部完成之後才能開始。
試想下,在這種情況下,join是沒辦法實現這個場景的,而CountDownLatch卻可以,因爲它持有一個計數器,只要計數器爲0,那麼主線程就可以結束阻塞往下執行。我們可以在worker0和worker1完成第一階段工作之後就把計數器減1即可,這樣worker0和worker1在完成第一階段工作之後,worker2就可以開始工作了。
worker:
- package com.concurrent.test5;
- import java.util.concurrent.CountDownLatch;
- /**
- * 工作者類
- * @author ThinkPad
- *
- */
- public class Worker extends Thread {
- //工作者名
- private String name;
- //第一階段工作時間
- private long time;
- private CountDownLatch countDownLatch;
- public Worker(String name, long time, CountDownLatch countDownLatch) {
- this.name = name;
- this.time = time;
- this.countDownLatch = countDownLatch;
- }
- @Override
- public void run() {
- // TODO 自動生成的方法存根
- try {
- System.out.println(name+"開始工作");
- Thread.sleep(time);
- System.out.println(name+"第一階段工作完成");
- countDownLatch.countDown();
- Thread.sleep(2000); //這裏就姑且假設第二階段工作都是要2秒完成
- System.out.println(name+"第二階段工作完成");
- System.out.println(name+"工作完成,耗費時間="+(time+2000));
- } catch (InterruptedException e) {
- // TODO 自動生成的 catch 塊
- e.printStackTrace();
- }
- }
- }
Test:
- package com.concurrent.test5;
- import java.util.concurrent.CountDownLatch;
- public class Test {
- public static void main(String[] args) throws InterruptedException {
- // TODO 自動生成的方法存根
- CountDownLatch countDownLatch = new CountDownLatch(2);
- Worker worker0 = new Worker("worker0", (long) (Math.random()*2000+3000), countDownLatch);
- Worker worker1 = new Worker("worker1", (long) (Math.random()*2000+3000), countDownLatch);
- Worker worker2 = new Worker("worker2", (long) (Math.random()*2000+3000), countDownLatch);
- worker0.start();
- worker1.start();
- countDownLatch.await();
- System.out.println("準備工作就緒");
- worker2.start();
- }
- }
觀察控制檯打印順序,可以發現這種方法是可以模擬場景2的:
worker0開始工作
worker1開始工作
worker1第一階段工作完成
worker0第一階段工作完成
準備工作就緒
worker2開始工作
worker1第二階段工作完成
worker1工作完成,耗費時間=5521
worker0第二階段工作完成
worker0工作完成,耗費時間=6147
worker2第一階段工作完成
worker2第二階段工作完成
worker2工作完成,耗費時間=5384
最後,總結下CountDownLatch與join的區別:調用thread.join() 方法必須等thread 執行完畢,當前線程才能繼續往下執行,而CountDownLatch通過計數器提供了更靈活的控制,只要檢測到計數器爲0當前線程就可以往下執行而不用管相應的thread是否執行完畢。