一、核心设计思想
1. 计数器机制
- 初始化:设定一个初始计数值(如
N)。 - 等待线程:调用
await()的线程会阻塞,直到计数器归零。 - 任务线程:每个任务线程完成工作后调用
countDown(),计数器减 1。
2. 一次性特性
- 不可重置:计数器归零后无法重复使用,若需重复协作,应选择
CyclicBarrier。
3. 典型场景
- 主从协作:主线程等待所有子线程完成初始化。
- 并行汇总:多个子任务并行执行,主线程等待所有子任务完成后再汇总结果。
二、核心方法与使用示例
1. 核心方法
| 方法 | 说明 |
|---|---|
CountDownLatch(int count) | 构造函数,初始化计数器。 |
void await() | 阻塞当前线程,直到计数器归零(支持可中断等待)。 |
boolean await(long timeout, TimeUnit unit) | 阻塞当前线程,超时后自动唤醒。 |
void countDown() | 计数器减 1(若计数器已为 0,则调用无效)。 |
long getCount() | 获取当前计数器值。 |
2. 示例:主线程等待子线程完成任务
public class MainSubThreadCoordination {
public static void main(String[] args) throws InterruptedException {
int threadCount = 3;
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
// 模拟任务执行
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + " 完成任务");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown(); // 确保计数器递减
}
}).start();
}
latch.await(); // 主线程等待所有子线程完成
System.out.println("所有任务完成,主线程继续执行");
}
}
三、底层实现:基于 AQS 的共享模式
CountDownLatch 的核心依赖于 AQS(AbstractQueuedSynchronizer) 的 共享模式,通过状态变量 state 表示计数器。
1. AQS 状态管理
- 初始化:
state被设置为初始计数值(如N)。 countDown():调用releaseShared(1),通过 CAS 将state减 1。await():调用acquireSharedInterruptibly(1),若state != 0,线程进入 CLH 队列阻塞。
2. 关键源码分析
// CountDownLatch.Sync 内部类(继承 AQS)
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
setState(count); // 初始化计数器
}
// 尝试获取共享锁:当 state == 0 时返回 1,否则返回 -1
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
// 尝试释放共享锁:CAS 递减 state
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; // 返回 true 表示计数器归零,唤醒等待线程
}
}
}
3. await() 的执行流程
- 检查
state是否为 0,若是则直接返回。 - 若
state > 0,将当前线程加入 CLH 队列并阻塞。 - 当某个线程调用
countDown()导致state归零时,唤醒队列中所有等待线程。
4. countDown() 的执行流程
- 通过 CAS 将
state减 1。 - 若
state归零,调用AQS.doReleaseShared()唤醒所有等待线程。
四、使用场景与最佳实践
1. 多线程初始化
// 服务启动时等待所有组件初始化完成
CountDownLatch latch = new CountDownLatch(3);
new Thread(() -> { initDB(); latch.countDown(); }).start();
new Thread(() -> { initCache(); latch.countDown(); }).start();
new Thread(() -> { initMQ(); latch.countDown(); }).start();
latch.await(); // 等待所有组件初始化
startServer();
2. 并行任务拆分
// 将大任务拆分为多个子任务并行处理
List<String> data = fetchData(); // 获取数据
CountDownLatch latch = new CountDownLatch(data.size());
for (String item : data) {
new Thread(() -> {
process(item); // 处理单个数据项
latch.countDown();
}).start();
}
latch.await(); // 等待所有子任务完成
generateReport(); // 生成汇总报告
3. 超时控制
if (latch.await(10, TimeUnit.SECONDS)) {
System.out.println("所有任务在10秒内完成");
} else {
System.out.println("部分任务超时未完成");
}
五、常见问题与解决方案
1. 计数器不可重置
- 问题:
CountDownLatch的计数器归零后无法重用。 - 解决:若需重复使用,改用
CyclicBarrier。
2. 死锁风险
- 场景:子线程未正确调用
countDown()(如异常未执行到finally块)。 - 解决:确保
countDown()在finally块中调用。try { // 任务代码 } finally { latch.countDown(); // 必须执行 }
3. 线程数不足
- 错误示例:初始化
CountDownLatch(3),但只有 2 个线程调用countDown()。 - 解决:确保线程数与计数器值匹配。
六、CountDownLatch vs 其他同步工具
| 工具 | 核心特性 | 适用场景 |
|---|---|---|
| CountDownLatch | 一次性,主线程等待子线程完成。 | 主从协作、并行任务汇总 |
| CyclicBarrier | 可重置,线程互相等待到达屏障点。 | 并行任务分阶段协同 |
| CompletableFuture | 支持链式异步任务编排,可组合多个任务。 | 复杂异步流程控制 |
| Phaser | 动态注册/注销线程,支持多阶段协同。 | 分阶段动态调整线程协作 |
七、性能优化与注意事项
- 合理设置计数器值:避免过大(内存开销)或过小(无法发挥并发优势)。
- 避免长时间阻塞:结合
await(timeout, unit)防止线程永久挂起。 - 线程池配合:使用线程池管理任务线程,避免频繁创建/销毁线程。
ExecutorService executor = Executors.newFixedThreadPool(4); CountDownLatch latch = new CountDownLatch(10); for (int i = 0; i < 10; i++) { executor.submit(() -> { try { // 任务代码 } finally { latch.countDown(); } }); } latch.await(); executor.shutdown();
总结
CountDownLatch 通过简洁的计数器模型,为多线程协作提供了高效且直观的解决方案。其基于 AQS 的共享模式实现确保了高性能与线程安全性。