CountDownLatch 是 Java 并发编程中的一个同步工具类,这个工具经常用来用来协调多个线程之间的同步,下面我们就来一起认识一下 CountDownLatch。
CountDownLatch介绍
应用场景
CountDownLatch主要是用于让一个或多个线程等待其他线程完成某些操作后再继续执行。例如,小组早上开会,组长(主线程)只有等所有人(每个人代表一个线程)到达会议室才能开;再如,游乐园里的过山车,一次可以坐10个人,为了节约成本,通常是等够10个人了才开。
CountDownLatch的核心思想是“倒计时”:线程在执行之前,需要等到计数器倒数到零。所以CountDownLatch的用法通常是设定一个大于0的值,该值即代表需要等待的总任务数,每完成一个任务后,将总任务数减一,直到最后该值为0,说明所有等待的任务都执行完了,后面的任务可以继续执行。
核心方法
CountDownLatch类的核心方法有以下几个。
构造函数:CountDownLatch的构造函数需要传入一个int类型参数count,该参数表示需要倒数的值。
public CountDownLatch(int count)
...
}
await():调用 await() 方法的线程开始阻塞,直到倒计时结束,也就是 count 值为 0 的时候才会继续执行。await方法还有一个版本await(long timeout, TimeUnit unit),这个版本可以设置超时时间,如果超时就不再等待了。
countDown():每当一个线程完成了某个操作,它就调用 countDown() 方法将计数器减一,如果减到 0 时,之前等待的线程会被唤起。
public void countDown()
public boolean await(long timeout, TimeUnit unit) throws InterruptedException
CountDownLatch用法
一个线程等待其他多个线程都执行完毕
举个生活中的例子,张老师是一个对学生非常严格的老师,在她的班级里,只有所有学生当天的作业都完成了,张老师才会放学,我们就可以用CountDownLatch来模拟这个场景,为了简单起见,假设班里只有3个学生,代码如下:
class DoHomeworkRunnable implements Runnable{
CountDownLatch countDownLatch = null;
private int i;
public DoHomeworkRunnable(CountDownLatch countDownLatch, int i) {
this.countDownLatch = countDownLatch;
this.i = i;
}
@Override
public void run() {
try {
Thread.sleep((i+1)*1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println((i+1) + "号同学完成了作业");
countDownLatch.countDown();
}
}
//一个线程等待其他多个线程都执行完毕,再继续自己的工作
public class CountDownLatchExample1 {
public static void main(String[] args) throws InterruptedException {
long startTime = Calendar.getInstance().getTimeInMillis();
CountDownLatch countDownLatch = new CountDownLatch(3);
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i=0; i<3; i++){
executorService.submit(new DoHomeworkRunnable(countDownLatch, i));
}
System.out.println("等待所有同学都完成 作业");
countDownLatch.await();
System.out.println("所有同学都完成作业,放学");
long endTime = Calendar.getInstance().getTimeInMillis();
System.out.println("程序执行了 " + (endTime - startTime) + " ms");
}
}
//输出
等待所有同学都完成 作业
1号同学完成了作业
2号同学完成了作业
3号同学完成了作业
所有同学都完成作业,放学
程序执行了 3018 ms
在这段代码中,我们新建了一个初始值为3的CountDownLatch,然后建立了线程数为3的线程池,往线程池中提交3个任务,每个任务代表一个学生,这个学生会sleep一段时间,模拟写作业的过程,然后打印出他完成了作业,在每个学生完成作业之后,都会调用 countDown 方法来把计数减 1。
主线程中会调用 await() 方法,在count值变为 0之前会让主线程等待,直到几个子线程都执行完毕都 完成了作业后,它才会继续执行打印出 “所有同学都完成作业,放学”。为什么程序执行了3s左右,这是因为这3个任务中,3号学生执行了3s。
可以用下面的流程图帮助我们理解:
多个线程等待某一个线程的信号
这个用法和上面的用法有点相反,还是拿老师和同学的场景举例:打完上课铃之后,老师会等待同学们5s,等待同学都进入教室,然后喊起立,只有老师喊完起立后,同学们才会同时站起来。代码如下:
class ReadyRunnable implements Runnable{
CountDownLatch countDownLatch = null;
private int i;
public ReadyRunnable(CountDownLatch countDownLatch, int i) {
this.countDownLatch = countDownLatch;
this.i = i;
}
@Override
public void run() {
System.out.println((i+1) + "号同学进入教室 ,等待老师喊起立");
try {
countDownLatch.await();
System.out.println((i+1) + "号同学站起");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//多个线程等待某一个线程的信号,同时开始执行
public class CountDownLatchExample2 {
public static void main(String[] args) throws InterruptedException {
long startTime = Calendar.getInstance().getTimeInMillis();
CountDownLatch countDownLatch = new CountDownLatch(1);
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i=0; i<3; i++){
executorService.submit(new ReadyRunnable(countDownLatch, i));
}
Thread.sleep(5000);
countDownLatch.countDown();
System.out.println("5秒准备时间已过,老师喊起立!");
long endTime = Calendar.getInstance().getTimeInMillis();
System.out.println("程序执行了 " + (endTime - startTime) + " ms");
}
}
//输出
1号同学进入教室 ,等待老师喊起立
3号同学进入教室 ,等待老师喊起立
2号同学进入教室 ,等待老师喊起立
5秒准备时间已过,老师喊起立!
1号同学站起
3号同学站起
2号同学站起
程序执行了 5020 ms
在这段代码中,新建了一个 CountDownLatch,其倒数值只有 1;然后建立了线程数为3的线程池,往线程池中提交3个任务,而这3个任务在一开始时就让它调用 await() 方法开始等待。
主线程sleep5 秒,等待同学们进入教室,5 秒之后,主线程调用countDown()将倒数值减为0,然后老师喊起立,此时,之前那 3 个已经调用了 await() 方法的子线程都会被唤醒执行,同时打印出"x号同学站起"。
同样画了一张流程图帮助理解: