CountDownLatch 是 Java 1.5 版本开始提供在 java.util.concurrent
包下的一个同步工具类,用来协调多个线程之间的同步。
CountDownLatch 能够使一个线程在等待另外一些线程各自完成工作之后,再继续执行。可以把它看成是一个计数器,其内部维护着一个 count 计数,只不过对这个计数器的操作都是原子操作,同时只能有一个线程去操作这个计数器。计数器初始值为线程的数量。当每一个线程完成自己任务后,计数器的值就会减 1。当计数器的值为 0 时,表示所有的线程都已经完成了任务,然后在 CountDownLatch 上等待的线程就可以恢复执行任务。
CountDownLatch 原理
CountDownLatch 是通过一个计数器来实现的,计数器的初始化值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就相应得减 1。当计数器到达 0 时,表示所有的线程都已完成任务,然后在闭锁上等待的线程就可以恢复执行任务。
CountDownLatch 用法及场景
CountDownLatch 主要有 2 种典型的用法:
- 第一种是一个线程等待其他 n 个线程执行完成后再执行,即其他线程通过 countDown 来对计数器减1,最终唤醒主线程。
- 第二种是每个线程调用 countDownLatch 对象的 await 方法,等待主线程进行 countDown 来唤醒所有其他线程,与第一种相反。
第一种用法如下几个步骤:
- 在主线程上下文下,初始化一个共享的 CountDownLatch 对象,并指定需要等待的线程数(即 count 值);
- 每个其他线程在执行完相应逻辑之后,调用主线程上下文中的共享 countDownLatch 对象的 countDown 方法,将对计数器减 1;
- 在主线程上下文下,调用 countDownLatch 对象的 await 方法使主线程进入阻塞状态,当计数器为 0 时,唤醒主线程,并进行相应的逻辑。
第一种用法的典型场景,如应用程序的主线程希望在负责启动框架服务的线程已经启动所有框架服务之后执行。
第二种用法如下几个步骤:
- 初始化一个共享的 CountDownLatch(1),将其计数器设置为 1;
- 多个线程在执行任务之前调用 countDownLatch.await(),使线程进入阻塞状态,等待唤醒;
- 主线程调用 countDownLatch.countDown(),使计数器变为 0,多个线程同时被唤醒。
第二种用法的典型场景,如模拟并发请求。
第一种的用法是强调并行性,简言之,多个线程操作完成之前一直等待。
第二种的用法是在于并发性,简言之,同时启动多个线程。
CountDownLatch 例子
首先给出第一种用法的例子。
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
public class Main {
static class MyThread implements Runnable {
private CountDownLatch countDownLatch;
private String name;
private long millis;
public MyThread(CountDownLatch countDownLatch, String name, long millis) {
this.countDownLatch = countDownLatch;
this.name = name;
this.millis = millis;
}
@Override
public void run() {
//do something
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + " is done");
countDownLatch.countDown();
}
}
public static void main(String[] args) throws IOException, InterruptedException {
// 主线程的共享计数器
CountDownLatch countDownLatch = new CountDownLatch(3) {
@Override
public void await() throws InterruptedException {
super.await();
// 计数器为 0 时,唤醒主线程(为了演示,增加了日志输出)
System.out.println(Thread.currentThread().getName() + " count down is ok");
}
};
MyThread myThread1 = new MyThread(countDownLatch, "thread1", 1500);
MyThread myThread2 = new MyThread(countDownLatch, "thread2", 1000);
MyThread myThread3 = new MyThread(countDownLatch, "thread3", 2000);
// 并行运行多个线程
new Thread(myThread1).start();
new Thread(myThread2).start();
new Thread(myThread3).start();
// 主线程进行阻塞,等待计数器为 0
countDownLatch.await();
// 主线程执行其他线程执行完后的逻辑
System.out.println("do main thread things");
}
}
输出结果如下:
thread2 is done
thread1 is done
thread3 is done
main count down is ok
do main thread things
第二种用法与第一种用法正好相反,主线程来进行 countDown 来唤醒其他线程。
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
public class Main {
static class MyThread implements Runnable {
private CountDownLatch countDownLatch;
private String name;
public MyThread(CountDownLatch countDownLatch, String name) {
this.countDownLatch = countDownLatch;
this.name = name;
}
@Override
public void run() {
try {
// 线程进行阻塞,等待计数器为 0
countDownLatch.await();
System.out.println(name + " is doing something");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) throws IOException, InterruptedException {
// 主线程的共享计数器
CountDownLatch countDownLatch = new CountDownLatch(1);
MyThread myThread1 = new MyThread(countDownLatch, "thread1");
MyThread myThread2 = new MyThread(countDownLatch, "thread2");
MyThread myThread3 = new MyThread(countDownLatch, "thread3");
// 并行运行多个线程
new Thread(myThread1).start();
new Thread(myThread2).start();
new Thread(myThread3).start();
System.out.println("main thread before count down");
// 计数器减 1,即变为 0,唤醒其他线程
countDownLatch.countDown();
}
}
输出结果如下:
main thread before count down
thread3 is doing something
thread2 is doing something
thread1 is doing something
CountDownLatch 不足
CountDownLatch 是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 CountDownLatch 使用完毕后,它不能再次被使用。