别再 Thread.sleep 硬等了!并发工具类到底怎么选?

并发编程里,最折磨人的不是锁,而是""。

你等一个线程结果,用 join();等多个任务完成,写一堆 while + flag;想控制同时最多 3 个任务跑,自己维护计数器;想在某个点"齐步走",再加个自旋。最后代码丑不说,还全是竞态 bug。

Java 早就给你备好了四把趁手的工具:CountDownLatchCyclicBarrierSemaphoreCompletableFuture。这篇从真实场景出发,把这四个工具一次性讲透,看完就知道什么时候该用哪个

一、先看四把工具是干什么的

先给一张总览表,心里有个谱:

工具 核心语义 典型场景 线程间关系
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 的线程全部放行(一次性使用,不能重置)
  • 常用于「发令枪」或「汇总统计」两种模式

常见坑

  1. countDown 忘写 → 死等。建议放 finally。
  2. 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

七、总结

  1. CountDownLatch:等计数归零,一次性,主线程等子线程。
  2. CyclicBarrier:N 个线程互相等,可复用,齐了再一起走。
  3. Semaphore:许可制,控制并发资源占用,acquire/release 必须成对。
  4. CompletableFuture:异步编排王者,回调、超时、兜底一站式。

四类工具本质都是在回答同一个问题:多个线程什么时候能碰头、能同时干几件。把"等"这件事想明白,代码就不会写得那么痛苦了。

下一篇继续拆:线程池源码 ThreadPoolExecutor 的参数到底怎么调?ExecutorService 为什么别用 Executors 默认工厂?

相关推荐
SamDeepThinking42 分钟前
从REST到gRPC,一个API选型的思考框架
java·后端·程序员
Zane19941 小时前
接口都能写默认实现了,为什么还需要抽象类
java·后端
未秃头的程序猿1 小时前
读Spring AI源码的方法——不是硬啃,是带着问题去翻
java·后端·spring
Java内核笔记1 小时前
万字长文剖析 Spring Boot 4 自动配置机制源码:从 @EnableAutoConfiguration 到条件装配
java·后端
evans在进步1 小时前
Spring MVC 核心机制详解:全局异常处理、请求跳转与数据绑定
java·spring·mvc
weixin_BYSJ19871 小时前
springboot小区物业管理小程序---附源码45555
java·javascript·spring boot·python·django·flask·php
coder_lorraine1 小时前
Halo 2.x 回收站文章无法删除?一篇文章教你彻底解决
java·运维
索隆zoro2 小时前
Army 进阶:DML 操作体系与 Spring AI 集成
java
吃饱了得干活2 小时前
为什么你的Service越写越臃肿?三层架构的“业务逻辑层”是个黑盒
java·后端·架构