CountDownLatch看作一个计数器,只不过这个计数器的操作是原子操作,同时只能有一个线程去操作这个计数器,也就是同时只能有一个线程去减这个计数器里面的值。CountDownLatch的一个非常典型的应用场景是:有一个任务想要往下执行,但必须要等到其他的任务执行完毕后才可以继续往下执行。假如我们这个想要继续往下执行的任务调用一个CountDownLatch对象的await()方法,其他的任务执行完自己的任务后调用同一个CountDownLatch对象上的countDown()方法,这个调用await()方法的任务将一直阻塞等待,直到这个CountDownLatch对象的计数值减到0为止。 所以,CountDownLatch类可以用于控制多个线程同时开始运行,或者用于主线程等待所有子线程的某个执行结果。一句话就是它会在倒数结束之前像门闩一样阻止线程的前进(就是阻塞)。它的用途和Thread.join()方法类似,都是用于线程间的协作。在一些场景,比如你需要用线程池去启用多个线程并发地做一些处理,但是这之后的工作又必须要等这些并发的任务都完成之后再继续。在这时你就可以使用CountDownLatch了。
例:百米赛跑:8名运动员同时起跑,由于速度的快慢,肯定有会出现先到终点和晚到终点的情况,而终点有个统计成绩的仪器,当所有选手到达终点时,它会统计所有人的成绩并进行排序,然后把结果发送到汇报成绩的系统。 其实这就是一个CountDownLatch的应用场景:一个线程或多个线程等待其他线程运行达到某一目标后进行自己的下一步工作,而被等待的“其他线程”达到这个目标后继续自己下面的任务。实例中的EndCounter等待所有worker线程结束赛跑过程后将endSignal变为0,在此之前EndCounter一直处于等待状态,在beginSignal被EndCounter变为0之前所有worker都处于就绪状态的线程继续处于等待状态。总之,EndCounter将beginSignal变为0之后,worker才可以执行,这样就可以使得所有worker同时执行他们的任务,而在worker将endSignal变为0之前处于等待状态,这使得EndCounter得到所有worker的执行结果之后再往下执行,但是当worker将endCounter减一之后可以继续执行自己后面的任务。
这个场景中:
1. 被等待的“其他线程”------>8名运动员
2. 等待“其他线程”的这个线程------>终点统计成绩的仪器
那么,如何来通过CountDownLatch来实现上述场景的线程控制和调度呢?CountDownLatch类有一个常用的构造方法:CountDownLatch(int count); 两个常用的方法:await()和countdown() ;其中count是一个计数器中的初始化数字,比如初始化的数字是8,当一个线程里调用了countdown(),则这个计数器就减一,当线程调用了 await(),则这个线程就等待这个计数器变为0,当这个计数器变为0时,这个线程继续自己下面的工作。程序源代码如下所示:
Worker源代码:
import java.util.concurrent.CountDownLatch;
public class Worker implements Runnable {
private int workerID;
private CountDownLatch beginSignal;
private CountDownLatch endSignal;
public Worker(int workerID, CountDownLatch begin, CountDownLatch end) {
this.workerID = workerID;
this.beginSignal = begin;
this.endSignal = end;
}
@Override
public void run() {
try {
// 所有work线程都在此处等待beginSingle变为0,在beginSignal变为0之前所有worker必须等待
beginSignal.await();
System.out.println("worker-" + workerID + "开始起跑...");
System.out.println("worker-" + workerID + "到达终点");
endSignal.countDown();// 将endSignal--
System.out.println("worker-" + workerID + "继续执行自己的任务");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
EndCounter源代码:
import java.util.concurrent.CountDownLatch;
public class EndCounter {
public static void main(String[] args) {
//beginSignal用于主线程控制处理worker统一执行
CountDownLatch beginSignal = new CountDownLatch(1);
//endSignal用于主线程等待worker线程的执行结果,得到结果之后主线程继续向下执行
CountDownLatch endSignal = new CountDownLatch(8);
for (int i = 0; i < 8; i++) {
new Thread(new Worker(i + 1, beginSignal, endSignal)).start();
}
try {
System.out.println("主面试官即将开始发布起跑命令");
beginSignal.countDown(); // begSignal变为0,所有worker线程统一起跑
endSignal.await(); // 等待运动员到达终点,即endSignal变为0
System.out.println("结果发送到汇报成绩的系统");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
执行结果(其中一种情况):
主面试官即将开始发布起跑命令
worker-1开始起跑...
worker-1到达终点
worker-1继续执行自己的任务
worker-2开始起跑...
worker-2到达终点
worker-2继续执行自己的任务
worker-5开始起跑...
worker-3开始起跑...
worker-6开始起跑...
worker-6到达终点
worker-6继续执行自己的任务
worker-4开始起跑...
worker-4到达终点
worker-4继续执行自己的任务
worker-8开始起跑...
worker-7开始起跑...
worker-7到达终点
worker-7继续执行自己的任务
worker-3到达终点
worker-5到达终点
worker-3继续执行自己的任务
worker-8到达终点
worker-5继续执行自己的任务
结果发送到汇报成绩的系统
worker-8继续执行自己的任务
关于CountDownLatch、CyclicBarrier、Semaphore的几点说明:如果是一个线程等待一个线程,则可以通过await()和notify()来实现;如果是一个线程等待多个线程,则就可以使用CountDownLatch和CyclicBarrier来实现比较好的控制。 CyclicBarrier可以循环利用,也就是在到达屏障之后,线程就会被销毁,而CountDownLatch则不会,各个线程还继续执行自己的任务。CountDownLatch是等大家都跑完了再继续,CyclicBarrier是等大家准备好了一起跑,Semaphore是只允许一定数量的跑。