countDownLatch和join的區別(轉載)
首先,我們來看一個應用場景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(); } } }
Test類如下:
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(); } }
執行test,觀察控制檯輸出的順序,我們發現這樣可以滿足需求,worker2確實是等worker0和worker1完成之後才開始工作的:
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();
}
}
}
Test:
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是否執行完畢。