多线程协作利器:CountDownLatch 核心用法与场景解析

在日常开发中,我们经常会遇到需要协调多个线程任务的场景。比如,主线程需要等待所有子线程处理完毕后再进行汇总,或者多个线程需要等待某个初始化操作完成后才能开始工作。直接控制线程的等待与唤醒既复杂又容易出错。

今天,我们来介绍一个非常实用的并发工具------CountDownLatch,它可以优雅地解决上述问题。

一、CountDownLatch 是什么?

CountDownLatchjava.util.concurrent 包下的一个同步工具类。它允许一个或多个线程等待其他一组线程完成操作。

你可以把它理解为一个计数器 ,这个计数器的初始值在创建时被设定。线程通过调用 await() 方法进入等待状态,直到计数器的值被其他线程通过 countDown() 方法减到零,等待的线程才会被唤醒继续执行。

它的核心特点非常明确:

  1. 计数单向性:计数器只能减少,不能增加。
  2. 一次性 :当计数器减到零后,再调用 await() 方法会立即返回,无法重置再次使用。

二、核心方法与工作机制

CountDownLatch 的 API 非常简单,主要依赖两个核心方法:

  • await():使当前线程进入等待状态,直到计数器变为零。除非线程被中断。
  • countDown():将计数器的值减 1。如果计数器达到零,将释放所有等待的线程。

工作机制简述:

  1. 初始化 CountDownLatch 对象,设定一个正整数作为计数器初始值(例如 N)。
  2. 主线程调用 await() 方法后会被阻塞。
  3. 每个子线程在执行完任务后,调用 countDown() 方法,使计数器减 1。
  4. 当所有 N 个子线程都执行完毕并调用了 countDown() 后,计数器归零,主线程被唤醒并继续执行。

三、主要应用场景

CountDownLatch 在实际项目中有着广泛的应用,以下是一些典型场景:

1. 主线程等待所有子任务完成

这是最经典的场景。将一个大型任务拆分为多个子任务并行处理,主线程需要等待所有子任务完成后,才能进行结果合并或后续操作。

scss 复制代码
// 模拟一个需要处理5个子任务的主线程
CountDownLatch latch = new CountDownLatch(5);

for (int i = 0; i < 5; i++) {
    new Thread(() -> {
        try {
            // 模拟子任务执行
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + " 完成任务");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 任务完成,计数器减1
            latch.countDown();
        }
    }).start();
}

// 主线程等待所有子任务完成
latch.await();
System.out.println("所有子任务已完成,主线程开始汇总结果...");

2. 实现多个线程的并发启动

在某些性能测试或需要"同时发令"的场景下,可以使用 CountDownLatch 作为发令枪,让多个线程在同一时刻开始执行任务。

scss 复制代码
// 发令枪,初始为1,所有线程都在等待这一声枪响
CountDownLatch startSignal = new CountDownLatch(1);

for (int i = 0; i < 10; i++) {
    new Thread(() -> {
        try {
            // 所有线程都在此等待
            startSignal.await();
            // 收到信号,开始真正的工作
            doWork();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
}

// 主线程准备完毕后,鸣枪
Thread.sleep(1000); // 模拟准备时间
startSignal.countDown(); // 所有等待线程同时开始执行

3. 等待共享资源初始化

当多个线程依赖于一个共享资源,而这个资源初始化较慢时,可以让这些线程等待资源初始化完成后再开始工作。

scss 复制代码
// 资源初始化计数器,初始为1
CountDownLatch resourceLatch = new CountDownLatch(1);

// 模拟资源初始化线程
new Thread(() -> {
    initializeSharedResource(); // 耗时的初始化操作
    resourceLatch.countDown(); // 初始化完成,释放所有等待线程
}).start();

// 其他工作线程
new Thread(() -> {
    resourceLatch.await(); // 等待资源就绪
    useSharedResource(); // 使用已初始化的资源
}).start();

四、重要注意事项

  1. 计数器无法重置CountDownLatch 是一次性的,计数器归零后就无法再次使用。如果需要循环使用的场景,请考虑使用 CyclicBarrier
  2. 确保计数器被减少 :务必在子线程中使用 try-finally 块调用 countDown(),以确保即使线程执行异常,计数器也能被减少,避免主线程无限期等待。
  3. 与 Spring 事务的陷阱 :在 Spring 管理的多线程场景中需要特别注意。@Transactional 注解的事务控制是基于 ThreadLocal 的,它不会跨线程传播。这意味着:
    • 主线程的事务和子线程的事务是完全独立的。
    • 如果主线程先执行了数据库操作,然后子线程在执行中抛出异常,主线程的操作不会回滚,这会导致数据不一致。
    • 解决方案通常是将子线程的业务也包装在独立的事务中,并通过更复杂的机制(如事务事件监听、分布式事务)来协调,而不能简单地依赖 CountDownLatch

总结

CountDownLatch 是一个设计精巧、功能明确的线程同步工具。它通过一个简单的计数器模型,高效地解决了"一个线程等待多个线程"和"多个线程等待一个信号"的协作问题。理解其原理并正确使用它,能够让你的多线程程序更加健壮和高效。

希望这篇文章能帮助你更好地理解和使用 CountDownLatch

相关推荐
天天摸鱼的java工程师2 小时前
支付回调处理,咱得整得 “幂等可靠” 不翻车
java·后端
踏浪无痕2 小时前
高并发写入 API 设计:借鉴 NSQ 的内存队列与背压机制
后端·面试·go
⑩-2 小时前
Spring 事务失效
java·后端·spring
BingoGo2 小时前
告别 Shell 脚本:用 Laravel Envoy 实现干净可复用的部署
后端
Cache技术分享2 小时前
267. Java 集合 - Java 开发必看:ArrayList 与 LinkedList 的全方位对比及选择建议
前端·后端
2501_921649492 小时前
亚太股票数据API:日股、韩股、新加坡股票、印尼股票市场实时行情,实时数据API-python
开发语言·后端·python·websocket·金融
爱上妖精的尾巴3 小时前
6-1WPS JS宏 new Set集合的创建
前端·后端·restful·wps·js宏·jsa
在坚持一下我可没意见3 小时前
Spring 后端安全双剑(上篇):JWT 无状态认证 + 密码加盐加密实战
java·服务器·开发语言·spring boot·后端·安全·spring
就像风一样抓不住3 小时前
SpringBoot静态资源映射:如何让/files/路径访问服务器本地文件
java·spring boot·后端