学完这篇,你能分清 CountDownLatch、CyclicBarrier、Semaphore 各自解决什么问题,并写出可直接运行的协作代码。
第一步:先搞清楚这三个工具分别在"等什么"
三个类都在 java.util.concurrent 包里,都是同步辅助器,但语义完全不同:
- CountDownLatch:一个(或几个)线程等 N 个事件完成,计数减到 0 就放行,一次性。
- CyclicBarrier:N 个线程互相等待,全部到齐后一起继续,可循环复用。
- Semaphore:控制同时访问某个资源的线程数量,靠"许可证"进出,可长期使用。
一句话记:Latch 是"等别人干完",Barrier 是"大家等大家",Semaphore 是"限流排队"。
第二步:CountDownLatch —— 主线程等子任务全部就绪
典型场景:启动时初始化多个资源,全部完成后再对外提供服务。
int n = 3;
CountDownLatch latch = new CountDownLatch(n);
for (int i = 0; i < n; i++) {
final int id = i;
new Thread(() -> {
try {
Thread.sleep(500L * id);
System.out.println("任务" + id + "完成");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown(); // 必须放 finally
}
}).start();
}
latch.await(); // 阻塞直到计数为 0
System.out.println("全部就绪,开始主流程");
要点:new CountDownLatch(n) 的 n 必须和 countDown() 的总调用次数一致;构造后计数不能重置。
注意:countDown() 一定写在 finally 里。任务抛异常时若漏掉这一句,await() 会永久卡死——这类线上"假死"排查起来非常痛苦。
await(long timeout, TimeUnit unit) 返回 boolean,超时返回 false,生产代码建议带上超时:
if (!latch.await(3, TimeUnit.SECONDS)) {
// 降级处理:部分资源未就绪
}
第三步:CyclicBarrier —— 多线程分阶段推进
典型场景:多线程并行计算,每一阶段结束要汇总一次,再进入下一阶段。
int parties = 3;
CyclicBarrier barrier = new CyclicBarrier(parties,
() -> System.out.println("== 一轮全部到齐,汇总数据 =="));
for (int i = 0; i < parties; i++) {
final int id = i;
new Thread(() -> {
try {
for (int round = 1; round <= 2; round++) {
System.out.println("线程" + id + " 第" + round + "轮干活");
Thread.sleep(200L * (id + 1));
barrier.await(); // 到齐后一起放行,并触发 barrierAction
}
} catch (Exception e) {
Thread.currentThread().interrupt();
}
}).start();
}
关键差异:await() 被调用 parties 次后,计数自动归零并进入下一轮,不需要重建对象,这就是"Cyclic"的含义。
注意:某一个线程在 await() 时被中断或超时,屏障会被标记为 broken,其余线程会收到 BrokenBarrierException。捕获时不要只写 InterruptedException,用 Exception 或同时捕获两个。
第四步:Semaphore —— 限制并发数
典型场景:数据库连接池、外部接口限流,最多允许 N 个线程同时进入。
Semaphore semaphore = new Semaphore(2); // 2 个许可证
for (int i = 0; i < 6; i++) {
final int id = i;
new Thread(() -> {
try {
semaphore.acquire(); // 拿不到就阻塞
System.out.println("线程" + id + " 进入,剩余许可 "
+ semaphore.availablePermits());
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
semaphore.release(); // 必须归还
}
}).start();
}
release() 同样要放 finally,否则许可证泄漏,后面所有线程都会被永久堵住。
注意:release() 并不校验是不是"自己拿的",多调一次就会凭空多一个许可证,限流直接失效。要防这种情况,用 acquire() 成功后立刻记一个标记位,或用 tryAcquire(timeout) 明确返回值。
需要公平排队(先到先得)时用 new Semaphore(2, true),代价是吞吐略降。另外 tryAcquire() 不阻塞,适合"拿不到就走降级逻辑"的场景。
第五步:怎么选
| 需求 | 用哪个 |
|---|
| 主线程等 N 个子任务结束 | CountDownLatch |
| 一批线程分阶段同步推进 | CyclicBarrier |
| 限制同时访问资源的线程数 | Semaphore |
| 需要重复使用的一次性等待 | 用 CyclicBarrier 或每次重建 Latch |
还有一个常见误区:这三个工具都不负责管理线程生命周期,它们只是同步协调。真正的线程池交给 ExecutorService,别为了用它们而裸写 new Thread()。
小结
- CountDownLatch 是"一等多",计数到 0 放行,不可重置,
countDown() 必须写在 finally。
- CyclicBarrier 是"多等多",到齐后自动开启下一轮,适合分阶段并行计算,注意
BrokenBarrierException。
- Semaphore 是"限并发",
acquire / release 必须成对,许可证泄漏会导致线程永久阻塞。
- 三者都建议配超时或
tryAcquire,避免无限期挂起;线程管理交给线程池。