目录
一、介绍
CountDownLatch是 Java 中的一个并发工具类,用于协调多个线程之间的同步。其作用是让某一个线程等待多个线程的操作完成之后再执行。它可以使一个或多个线程等待一组事件的发生,而其他的线程则可以触发这组事件。
二、特性
1. CountDownLatch可以用于控制一个或多个线程等待多个任务完成后再执行。
2. CountDownLatch的计数器只能够被减少,不能够被增加。
3. CountDownLatch的计数器初始值为正整数,每次调用countDown()方法会将计数器减 1,计数器为 0 时,等待线程开始执行。
三、实现原理
CountDownLatch的实现原理比较简单,它主要依赖于AQS(AbstractQueuedSynchronizer)框架来实现线程的同步。
CountDownLatch内部维护了一个计数器,该计数器初始值为N,代表需要等待的线程数目,当一个线程完成了需要等待的任务后,就会调用countDown()方法将计数器减 1,当计数器的值为 0 时,等待的线程就会开始执行。
四、适用场景
1. 主线程等待多个子线程完成任务后再继续执行。例如:一个大型的任务需要被拆分成多个子任务并交由多个线程并行处理,等所有子任务都完成后再将处理结果进行合并。
2. 启动多个线程并发执行任务,等待所有线程执行完毕后进行结果汇总。例如:在一个并发请求量比较大的 Web 服务中,可以使用CountDownLatch控制多个线程同时处理请求,等待所有线程处理完毕后将结果进行汇总。
3. 线程 A 等待线程 B 执行完某个任务后再执行自己的任务。例如:在多线程中,一个节点需要等待其他节点的加入后才能执行某个任务,可以使用CountDownLatch控制节点的加入,等所有节点都加入完成后再执行任务。
4. 多个线程等待一个共享资源的初始化完成后再进行操作。例如:在某个资源初始化较慢的系统中,可以使用CountDownLatch控制多个线程等待共享资源初始化完成后再进行操作。
CountDownLatch适用于多线程任务的协同处理场景,能够有效提升多线程任务的执行效率,同时也能够降低多线程任务的复杂度和出错率。
五、注意事项
1. CountDownLatch对象的计数器只能减不能增,即一旦计数器为 0,就无法再重新设置为其他值,因此在使用时需要根据实际需要设置初始值。
2. CountDownLatch的计数器是线程安全的,多个线程可以同时调用countDown()方法,而不会产生冲突。
3. 如果CountDownLatch的计数器已经为 0,再次调用countDown()方法也不会产生任何效果。
4. 如果在等待过程中,有线程发生异常或被中断,计数器的值可能不会减少到 0,因此在使用时需要根据实际情况进行异常处理。
5. CountDownLatch可以与其他同步工具(如Semaphore、CyclicBarrier)结合使用,实现更复杂的多线程同步。
六、实际应用
1. 案例一
(1) 场景
一个简单的CountDownLatch示例,演示了如何使用CountDownLatch实现多个线程的同步。
(2) 代码
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* CountDownLatchCase1
* 如何使用CountDownLatch实现多个线程的同步。
*
* @author wxy
* @since 2023-04-18
*/
public class CountDownLatchCase1 {
private static final Logger LOGGER = LoggerFactory.getLogger(CountDownLatchCase1.class);
public static void main(String[] args) throws InterruptedException {
// 创建 CountDownLatch 对象,需要等待 3 个线程完成任务
CountDownLatch latch = new CountDownLatch(3);
// 创建 3 个线程
Worker worker1 = new Worker(latch, "worker1");
Worker worker2 = new Worker(latch, "worker2");
Worker worker3 = new Worker(latch, "worker3");
// 启动 3 个线程
worker1.start();
worker2.start();
worker3.start();
// 等待 3 个线程完成任务
latch.await();
// 所有线程完成任务后,执行下面的代码
LOGGER.info("All workers have finished their jobs!");
}
}
class Worker extends Thread {
private static final Logger LOGGER = LoggerFactory.getLogger(Worker.class);
private final CountDownLatch latch;
public String name;
public Worker(CountDownLatch latch, String name) {
this.latch = latch;
this.name = name;
}
@Override
public void run() {
try {
// 模拟任务耗时
TimeUnit.MILLISECONDS.sleep(1000);
LOGGER.info("{} has finished the job!", name);
} catch (InterruptedException e) {
LOGGER.error(e.getMessage(), e);
} finally {
// 一定要保证每个线程执行完毕或者异常后调用countDown()方法
// 如果不调用会导致其他线程一直等待, 无法继续执行
// 建议放在finally代码块中, 防止异常情况下未调用countDown()方法
latch.countDown();
}
}
}
运行结果:
在上面的代码中,首先创建了一个CountDownLatch对象,并指定需要等待的线程数为 3。然后创建了 3 个线程并启动。每个线程会模拟执行一个耗时的任务,执行完成后会调用countDown()方法将计数器减 1。在所有线程都完成任务后,主线程会执行latch.await()方法等待计数器为 0,然后输出所有线程都完成任务的提示信息。
**思考:**如果不使用CountDownLatch情况将会是怎样呢?
运行结果:
由执行结果可知,主线程不会等待子线程结束后再执行。如果我们主线程(main) 需要其他线程执行后的结果,我们就需要使用countDownLantch让主线程和执行快的线程等待子线程全部执行完毕再向下执行。
**思考:**如果某个线程漏调用.countDown();会怎么样呢?
接下来我们模拟worker1线程异常,如果该线程异常latch.countDown()方法就无法被调用。
public void run() {
try {
// 模拟任务耗时
if ("worker1".equals(name)) {
throw new RuntimeException(name + "运行异常");
}
TimeUnit.MILLISECONDS.sleep(1000);
LOGGER.info("{} has finished the job!", name);
latch.countDown();
} catch (InterruptedException e) {
LOGGER.error(e.getMessage(), e);
}
}
运行结果:
由运行结果可知,当worker1线程由于异常没有执行countDown()方法,最后state结果不为0,导致所有线程停在AQS中自旋(死循环)。所以程序无法结束。(如何解决这个问题呢?请看案例二)
2. 案例二
(1) 场景
当年刚工作不久,遇到一个这样的问题:远程调用某个api,大部分情况下需要2-3s才能读取到响应值。我需要解析响应的JSON用于后续的操作。由于这个调用是异步的,我没办法在主线程获取到响应的JSON值。
当时第一时间想到的是让主线程休眠,但是休眠多久好呢?1、2、3s?显然是不行的,如果1s就请求成功并响应了,你要等3s,这不是浪费时间吗!
于是,我就请教了公司一位大佬。他告诉我使用CountDownLatch。我恍然大悟,之前自己学过,但是一到战场上我就把他给忘记了(实践是检验真理的唯一标准)。
(2) 代码(偷个懒 哈哈 就是在案例一的代码中修改了await()方法)
将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!");
看一下加了latch.await(5, TimeUnit.SECONDS)方法后执行结果:
博主介绍:上海交大毕业,大厂资深Java后端工程师,
《Java全套学习资料》作者,
专注于系统架构设计和高并发解决方案
阿里云开发社区乘风者计划专家博主,
CSDN平台Java领域优质创作者
常年分享Java技术干货、项目实战经验,
并为大学生和初学者提供项目实战与就业指导
擅长:分布式系统、SpringCloud、SpringBoot、Vue、MySQL、Redis、Docker等项目开发和设计
/**
* @author[vx] vip1024p(备注java)
* @【描述:浏览器打开】docs.qq.com/doc/DUkVoZHlPeElNY0Rw
*/
public class Hello {
public static void main(String[] args) {
System.out.println("Hello!!!");
}
}