并发编程里,最折磨人的不是锁,而是"等"。
你等一个线程结果,用 join();等多个任务完成,写一堆 while + flag;想控制同时最多 3 个任务跑,自己维护计数器;想在某个点"齐步走",再加个自旋。最后代码丑不说,还全是竞态 bug。
Java 早就给你备好了四把趁手的工具:CountDownLatch、CyclicBarrier、Semaphore、CompletableFuture。这篇从真实场景出发,把这四个工具一次性讲透,看完就知道什么时候该用哪个。
一、先看四把工具是干什么的
先给一张总览表,心里有个谱:
| 工具 | 核心语义 | 典型场景 | 线程间关系 |
|---|---|---|---|
| CountDownLatch | 计数器归零放行 | 主线程等 N 个子任务完成 | 一个等多个 |
| CyclicBarrier | 凑齐 N 个才齐步走 | 多线程分阶段同步 | 多个互相等 |
| Semaphore | 许可数控制并发 | 限流、连接池 | 多对多资源竞争 |
| CompletableFuture | 异步编排 + 回调 | 多任务组合、超时控制 | 任务流依赖 |
一句话记住:Latch 是"等人到齐了再走",Barrier 是"人齐了才一起走",Semaphore 是"只有 N 张票",Future 是"结果回来了叫你"。
二、CountDownLatch:等 N 个子任务完成
场景:并发预热 / 并发压测
要模拟 100 个用户同时访问接口,每个线程都做自己的请求,主线程要等所有请求发完,再统一统计耗时。
java
CountDownLatch ready = new CountDownLatch(1); // 发令枪
CountDownLatch done = new CountDownLatch(100); // 100 个线程全部完成
for (int i = 0; i < 100; i++) {
new Thread(() -> {
try {
ready.await(); // 所有线程先在这等发令
long start = System.currentTimeMillis();
httpClient.post("/api/order"); // 模拟并发请求
done.countDown(); // 干完一个,计数器减一
} catch (InterruptedException ignored) {}
}).start();
}
ready.countDown(); // 放行!100 个线程几乎同时开跑
done.await(); // 主线程等 100 个都完成
System.out.println("全部请求完成");
关键点:
await()阻塞等待计数器归零;countDown()每次减一- 计数器归零后,所有 await 的线程全部放行(一次性使用,不能重置)
- 常用于「发令枪」或「汇总统计」两种模式
常见坑
- countDown 忘写 → 死等。建议放 finally。
- await 不设超时 → 某个子任务挂了,主线程永远阻塞。生产环境务必
await(10, TimeUnit.SECONDS)。
java
// 正确姿势:加超时 + finally 兜底
try {
done.await(10, TimeUnit.SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
三、CyclicBarrier:人齐了才一起走
场景:多线程分片处理,各片完成后汇总
假设要把一个大文件拆 4 片,4 个线程各自处理一片,都处理完才能进入下一阶段(比如写入汇总表)。
java
CyclicBarrier barrier = new CyclicBarrier(4, () -> {
System.out.println("4 片都处理完了,进入下一阶段");
});
for (int i = 0; i < 4; i++) {
new Thread(() -> {
processSlice(Thread.currentThread().getName());
barrier.await(); // 等其它 3 片
// 屏障解除后,4 个线程继续各自的下一阶段
}).start();
}
和 CountDownLatch 的本质区别:
- Latch 是"倒数",归零后主线程继续,子线程任务结束;
- Barrier 是"集合点",N 个线程互相等,齐了之后一起继续 ,而且可以循环复用(reset 后继续下一轮)。
| 对比项 | CountDownLatch | CyclicBarrier |
|---|---|---|
| 谁在等 | 一个线程等多个 | N 个线程互相等 |
| 复用 | 不能复用(一次性) | 可以复用(重置) |
| 计数方向 | 减到 0 | 增加到 N |
| 破坏机制 | 无 | 超时/中断会抛 BrokenBarrierException |
坑点 :Barrier 里任何一个线程 await 超时或被中断,整个屏障会 Broken,其他线程全部抛 BrokenBarrierException。所以实际项目中要捕获处理。
四、Semaphore:只有 N 张许可证
场景:限流、数据库连接池
假设一个接口同时只能有 3 个任务在跑,多出的排队。用 Semaphore:
java
Semaphore semaphore = new Semaphore(3); // 3 个许可
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
semaphore.acquire(); // 拿许可,拿不到就排队
doHeavyWork(); // 最多同时 3 个在执行
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
semaphore.release(); // 释放许可,必须放 finally!
}
}).start();
}
坑点:
- acquire/release 必须成对,release 放 finally,否则少一个许可就永久少一个并发额度;
tryAcquire()不阻塞,拿不到立即返回 false,可用于"抢不到就降级"。- 还有
acquire(n)一次拿多个许可的场景(比如一个任务要占 2 个连接)。
和固定线程池的差异 :线程池限制的是线程数 ,Semaphore 限制的是许可数(可以限制资源而不限制线程),两者结合使用常见:线程池管线程生命周期,Semaphore 管同时占用资源的任务数。
五、CompletableFuture:异步编排 + 回调
场景:并行查多个接口再聚合
比如接口聚合三个来源:用户信息、订单列表、优惠券,三个可以并行查,等全部回来后组装。
java
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> userService.getUser(id));
CompletableFuture<List<Order>> orderFuture = CompletableFuture.supplyAsync(() -> orderService.list(id));
CompletableFuture<List<Coupon>> couponFuture = CompletableFuture.supplyAsync(() -> couponService.list(id));
CompletableFuture.allOf(userFuture, orderFuture, couponFuture)
.join(); // 三个都完成
User user = userFuture.get(); // 结果直接拿,不阻塞
List<Order> orders = orderFuture.get();
List<Coupon> coupons = couponFuture.get();
常用编排组合
| 方法 | 语义 |
|---|---|
thenApply(fn) |
上一个结果→新结果(同步) |
thenAccept(consumer) |
上一个结果→消费(无返回值) |
thenCompose(fn) |
上一个结果→新的 Future(异步链式) |
thenCombine(other, fn) |
两个 Future 都完成后合并 |
allOf(...) |
等所有完成,返回 void Future |
anyOf(...) |
等任一完成 |
orTimeout(timeout) |
超时自动完成(异常) |
completeOnTimeout(v, timeout) |
超时用默认值兜底 |
实战:带超时 + 兜底的聚合
java
CompletableFuture<Result> future = CompletableFuture.supplyAsync(() -> callThirdParty())
.orTimeout(3, TimeUnit.SECONDS) // 3 秒没结果算超时
.exceptionally(ex -> Result.defaultResult()); // 兜底:返回默认值
Result r = future.join();
坑点:
- 不带线程池时默认用
ForkJoinPool.commonPool(),全局共享,别在 IO 密集场景滥用,最好显式传线程池; join()抛的是CompletionException(unchecked),get()抛ExecutionException(checked),注意 catch。
六、怎么选:一张决策表
| 场景 | 首选工具 |
|---|---|
| 主线程等 N 个任务完成后继续 | CountDownLatch |
| N 个线程各自跑到屏障点再齐步走 | CyclicBarrier |
| 限流 / 控制并发资源占用 | Semaphore |
| 并行查多个接口再聚合 | CompletableFuture |
| 一个任务分成多个阶段异步链 | CompletableFuture.thenCompose |
| 多个异步任务,任一完成即可 | CompletableFuture.anyOf |
| 子线程需要把结果传回主线程 | Future / CompletableFuture |
七、总结
- CountDownLatch:等计数归零,一次性,主线程等子线程。
- CyclicBarrier:N 个线程互相等,可复用,齐了再一起走。
- Semaphore:许可制,控制并发资源占用,acquire/release 必须成对。
- CompletableFuture:异步编排王者,回调、超时、兜底一站式。
四类工具本质都是在回答同一个问题:多个线程什么时候能碰头、能同时干几件。把"等"这件事想明白,代码就不会写得那么痛苦了。
下一篇继续拆:线程池源码 ThreadPoolExecutor 的参数到底怎么调?ExecutorService 为什么别用 Executors 默认工厂?