别再 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 默认工厂?

相关推荐
linweidong6 小时前
中科闻歌Java面试题及参考答案
java·python·大模型·提示词·ai agent·mcp·rag原理
OpenAnolis小助手6 小时前
一句话看透 JVM,SysOM 诊断 Skill 新增 Java 应用诊断能力
java·开发语言·jvm·阿里云·操作系统控制台·sysom
ForteScarlet6 小时前
Kotlin 2.4.20 现已发布,新特性多不多?
android·java·开发语言·开源·kotlin·jetbrains
萧瑟余晖6 小时前
Java深入解析篇六十二之安全机制详解
java·开发语言
小刘在重生~7 小时前
Java 常用类|包装类 装箱拆箱、Integer 缓存面试题
java·python·缓存
Orange_sparkle7 小时前
Pi Agent vs LangGraph:从人机交互、企业 RAG 到持久化工作流的完整选型指南
java·网络·人机交互
Wang's Blog8 小时前
Java框架快速入门: Spring Security+OAuth2之过滤器与过滤器链
java·开发语言·spring
谢尔登9 小时前
01_Java基础速通
java·开发语言
lemon_sjdk9 小时前
javafx-Image图像(2)
java·javafx
小七在进步9 小时前
C++入门(2)
java·jvm·c++