彻底理解CountDownLatch

CountDownLatch 是 Java 并发包(java.util.concurrent)中一个非常经典且实用的同步工具类,由 Doug Lea 设计。它的核心思想是:

让一个或多个线程等待,直到其他线程完成一组操作("倒计时归零")后再继续执行。

下面从设计意图、内部实现、使用场景、内存语义四个维度帮你彻底理解它。


一、设计意图:解决什么问题?

场景1:主线程等所有子任务完成

  • 启动 N 个 worker 线程去处理任务。
  • 主线程不能继续,必须等这 N 个都做完。
  • CountDownLatch 初始化为 N,每个 worker 完成后调用 countDown(),主线程调用 await() 等待。

场景2:所有线程同时启动("发令枪")

  • 所有 worker 线程先启动,但卡在 await()
  • 主线程准备就绪后调用 countDown()(初始 count=1),所有 worker 同时开始。
  • ✅ 这就是"门闩"(latch)或"栅栏"的作用。

🔑 关键特性

  • 一次性(one-shot) :计数器只能从 N 减到 0,不能重置
  • 如果你需要重复使用(比如循环等待),请用 CyclicBarrier

二、内部实现:基于 AQS 的共享模式

CountDownLatch 的核心是内部类 Sync,它继承自 AbstractQueuedSynchronizer(AQS)。

1. 状态表示

java 复制代码
setState(count); // AQS 的 state 字段 = 倒计数值
  • state > 0:还有任务未完成,调用 await() 的线程要阻塞。
  • state == 0:所有任务完成,所有等待线程被释放。

2. 获取(await)------ 共享模式

java 复制代码
protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}
  • 返回 1:表示获取成功(可以通行)。
  • 返回 -1:表示获取失败(需要排队等待)。
  • 注意:只要 state == 0所有调用 await() 的线程都能立即通过 → 这就是共享模式的体现!

3. 释放(countDown)------ 共享释放

java 复制代码
protected boolean tryReleaseShared(int releases) {
    for (;;) {
        int c = getState();
        if (c == 0) return false;       // 已经归零,无需再释放
        int nextc = c - 1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;          // 只有归零时才触发唤醒
    }
}
  • 使用 CAS 循环安全地减 1。
  • 只有当 nextc == 0 时,才返回 true,通知 AQS:"现在可以唤醒所有等待者了!"

4. 唤醒机制

  • countDown() 使计数归零时,AQS 会调用 doReleaseShared()(你之前问过的那个方法)。
  • 因为是共享模式,所有等待线程都会被连续唤醒(传播机制确保不漏掉)。

✅ 所以:CountDownLatch 是 AQS 共享模式的经典应用


三、典型使用方式(结合你的代码注释)

示例1:发令枪 + 完成信号

java 复制代码
CountDownLatch startSignal = new CountDownLatch(1);  // 所有工人等"开始"信号
CountDownLatch doneSignal = new CountDownLatch(N);   // 主线程等"完成"信号

// 启动 N 个工人
for (int i = 0; i < N; i++)
    new Thread(() -> {
        startSignal.await();   // 阻塞,直到 startSignal.countDown()
        doWork();
        doneSignal.countDown(); // 完成一个,计数减1
    }).start();

doSomethingElse();         // 主线程做准备工作
startSignal.countDown();   // 发令:所有工人开始!
doneSignal.await();        // 等待所有工人完成

示例2:并行任务汇总

java 复制代码
ExecutorService exec = Executors.newFixedThreadPool(4);
CountDownLatch latch = new CountDownLatch(10);

for (int i = 0; i < 10; i++) {
    exec.submit(() -> {
        try {
            process(i);
        } finally {
            latch.countDown(); // 必须保证执行!
        }
    });
}

latch.await(); // 等待全部10个任务完成
exec.shutdown();

⚠️ 注意:countDown() 通常放在 finally 块中,防止异常导致计数不减,造成死锁。


四、内存可见性(Memory Consistency)

Java 内存模型(JMM)保证:

在某个线程调用 countDown() 之前的所有操作,对在 await() 成功返回后的线程是可见的。

即:happens-before 关系成立

这意味着:

  • Worker 线程写入的数据(如计算结果到共享变量),
  • 在主线程 await() 返回后,一定能读到最新值。

✅ 无需额外加 volatilesynchronized


五、与 CyclicBarrier 的区别

特性 CountDownLatch CyclicBarrier
是否可重用 ❌ 一次性 ✅ 可重复使用
触发条件 计数归零 所有线程到达屏障点
谁等待 任意线程(通常是协调者) 所有参与线程互相等待
典型用途 等待 N 个任务完成 / 发令枪 多线程迭代同步(如并行计算每轮同步)

六、总结:一句话理解 CountDownLatch

它是一个"倒计时门闩":门关着(count > 0),所有想通过的线程(调用 await)都得等着;每有人完成一件事(countDown),计数减一;当计数归零,门打开,所有等待者一拥而过,且之后来的人直接通行。

它的强大之处在于:

  • 简单、高效、线程安全;
  • 利用 AQS 共享模式实现"一对多"或"多对一"的同步;
  • 提供明确的内存可见性保证。

这也是为什么它成为并发编程中的"瑞士军刀"之一。

相关推荐
咩咩啃树皮6 小时前
第40篇:Vue3组件化开发精讲——组件拆分、复用、父子通信、工程化架构
java·前端·架构
灯澜忆梦7 小时前
GO_并发编程---定时器
开发语言·后端·golang
鱟鲥鳚7 小时前
Spring Boot 集成 LangChain4j:从模型调用到 Tool Calling(Demo版)
java·spring boot
-银雾鸢尾-7 小时前
C#中的StringBuilder相关方法
开发语言·c#
-银雾鸢尾-7 小时前
C#中结构体与类的区别;抽象类与接口的区别
开发语言·c#
大模型码小白8 小时前
【Python零基础教程】继承、多态与魔法函数:面向对象编程三大核心特性详解
java·大数据·开发语言·人工智能·python·ai编程
腾渊信息科技公司9 小时前
Spring Boot对接MES实战:视觉检测数据自动同步方案
java·人工智能·spring boot·后端·计算机视觉·ai·软件需求
爱笑的源码基地10 小时前
高并发 Redis 缓存门诊HIS系统源码,含财务统计药房进销存
java·程序·门诊系统·诊所系统·云诊所源码
wuqingshun31415910 小时前
TCP超时重传机制是为了解决什么问题?
java
段一凡-华北理工大学10 小时前
向量数据库实战:选型、调优与落地~系列文章12:文本分块策略实战:chunk_size 怎么选?重叠多少?
开发语言·数据库·后端·oracle·rust·工业智能体·高炉智能化