博主前段时间在公司写一个新需求的时候发现有同事写出了这么一串代码:

看到这块代码,博主傻眼了。看方法名 syncWait猜测应该是跟多线程有关,同步等待的。于是,博主立马去请教和学习了 CountDownLacth这个 JUC 包下的计时器类。下面,和大家一起再回顾下这个方法的使用!
CountDownLacth是 Java 中位于 JUC(java.util.concurrent)下的一个并发工具类,用于协调多个线程之间的同步。其作用是同步一个或多个线程。强制它们等待由其它线程执行的任务完成。
CountDownLacth底层是由 AQS提供支持,来实现线程间的同步。
CountDownLacth 内部维护了一个计数器,该计数器初始值为 N,代表需要等待的线程数目,当一个线程完成了需要等待的任务后,就会调用 countDown() 方法将计数器减 1,当计数器的值为 0 时,等待的线程就会开始执行。
CountDownLatch可以用于控制一个或多个线程等待多个任务完成后再执行。CountDownLatch的计数器只能够被减少,不能够被增加。CountDownLatch的计数器初始值为正整数,每次调用 countDown() 方法会将计数器减 1,计数器为 0 时,等待线程开始执行。总结:
CountDownLatch适用于多线程任务的协同处理场景,能够有效提升多线程任务的执行效率,同时也能够降低多线程任务的复杂度和出错率。
CountDownLatch对象的计数器只能减不能增,即一旦计数器为 0,就无法再重新设置为其他值,因此在使用时需要根据实际需要设置初始值。CountDownLatch的计数器是线程安全的,多个线程可以同时调用countDown()方法,而不会产生冲突。CountDownLatch的计数器已经为 0,再次调用countDown()方法也不会产生任何效果。countDown()方法来减少计数。CountDownLatch可以与其他同步工具(如 Semaphore、CyclicBarrier)结合使用,实现更复杂的多线程同步。下面演示一个简单的 CountDownLatch 示例,演示如何使用 CountDownLatch 实现多个线程的同步。
import java.util.concurrent.CountDownLatch;
public class testountDownLatch {
public static void main(String[] args) throws InterruptedException {
// 创建 CountDownLatch 对象,需要等待 3 个线程完成任务
CountDownLatch latch = new CountDownLatch(3);
// 创建三个子线程
Worker worker1 = new Worker(latch, "worker1");
Worker worker2 = new Worker(latch, "worker2");
Worker worker3 = new Worker(latch, "worker3");
// 启动三个子线程
worker1.start();
worker2.start();
worker3.start();
// 主线程等待三个子线程执行完毕
latch.await();
// 所有子线程执行完毕后,主线程开始执行
System.out.println("main 开始继续执行");
}
}
class Worker extends Thread {
private final CountDownLatch latch;
public String name;
public Worker(CountDownLatch latch, String name) {
this.latch = latch;
this.name = name;
}
@Override
public void run() {
// 假设这是每个子线程要执行的操作
try {
Thread.sleep(1000);
System.out.println("name = " + name);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
latch.countDown();
}
}
}运行结果:
name = worker3
name = worker2
name = worker1
main 开始继续执行
在上面的代码中,首先创建了一个CountDownLatch对象,并指定需要等待的线程数为 3。然后创建了 3 个线程并启动。每个线程会模拟执行一个耗时的任务,执行完成后会调用countDown()方法将计数器减 1。主线程会执行latch.await()方法一直等待子线程都执行完毕直到计数器为 0,然后输出所有线程都完成任务的提示信息。
思考一:如果这段代码中不使用 CountDownLatch 会怎么样?
运行结果:
main 开始继续执行
name = worker2
name = worker3
name = worker1
由执行结果可知,主线程不会等待子线程结束后再执行。如果我们主线程(main) 需要其他线程执行后的结果,我们就需要使用 countDownLantch 让主线程和执行快的线程等待子线程全部执行完毕再向下执行。
思考二:如果某个线程漏调用 .countDown() 方法会怎么样?
我们模拟 worker1 线程异常,让该线程异常时无法调用 .countDown()方法。
public void run() {
try {
// 模拟任务耗时
if ("worker1".equals(name)) {
throw new RuntimeException(name + "运行异常");
}
Thread.sleep(1000);
LOGGER.info("{} has finished the job!", name);
latch.countDown();
} catch (InterruptedException e) {
LOGGER.error(e.getMessage(), e);
}
}运行结果:

由运行结果可知,当 worker1 线程由于异常没有执行 countDown() 方法,最后计数器不为 0,导致所有线程停在AQS 中自旋(死循环)。所以程序无法结束。(如何解决这个问题呢?请看案例二)
案例一遗留下来的问题在于如果有一个线程未能成功释放计数器,就会导致所有线程自旋。那么我们的解决方法很明确,就是要让线程在某个时间点后自动的释放。
当时第一时间想到的是让主线程休眠,但是休眠多久好呢?1、2、3s?显然是不行的,如果1s就请求成功并响应了,你要等3s,这不是浪费时间吗!
于是,我就请教了公司一位大佬。他告诉我使用 await(long timeout, TimeUnit unit)。我恍然大悟,之前自己学过,但是一到战场上我就把他给忘记了。
将latch.await()修改为latch.await(5, TimeUnit.SECONDS),这段代码啥意思呢?就是当第一个线程到达 await() 方法开始计时,5s 后不等待未执行完毕的线程,直接向下执行。这么写的好处是,当调用某个方法超时太久,不影响我们的主逻辑。(很实用)
// 等待 3 个线程完成任务
if (!latch.await(5, TimeUnit.SECONDS)) {
LOGGER.warn("{} time out", worker1.name);
}
// 所有线程完成任务后,执行下面的代码
LOGGER.info("all workers have finished their jobs!");结果如下:

至此,问题全部解决。countDownLatch也已经带大家学习完毕!