多线程协作利器: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

相关推荐
Marktowin7 分钟前
玩转 ZooKeeper
后端
蓝眸少年CY35 分钟前
(第十二篇)spring cloud之Stream消息驱动
后端·spring·spring cloud
码界奇点1 小时前
基于SpringBoot+Vue的前后端分离外卖点单系统设计与实现
vue.js·spring boot·后端·spring·毕业设计·源代码管理
lindd9119111 小时前
4G模块应用,内网穿透,前端网页的制作第七讲(智能头盔数据上传至网页端)
前端·后端·零基础·rt-thread·实时操作系统·项目复刻
Loo国昌2 小时前
【LangChain1.0】第八阶段:文档处理工程(LangChain篇)
人工智能·后端·算法·语言模型·架构·langchain
vx_bisheyuange2 小时前
基于SpringBoot的海鲜市场系统
java·spring boot·后端·毕业设计
李慕婉学姐3 小时前
【开题答辩过程】以《基于Spring Boot和大数据的医院挂号系统的设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
大数据·spring boot·后端
源代码•宸3 小时前
Leetcode—3. 无重复字符的最长子串【中等】
经验分享·后端·算法·leetcode·面试·golang·string
0和1的舞者4 小时前
基于Spring的论坛系统-前置知识
java·后端·spring·系统·开发·知识
invicinble5 小时前
对于springboot
java·spring boot·后端