在 Java 高并发开发里,CompletableFuture 是异步编程与任务编排的核心工具。相比老旧的 Future 只能阻塞等待结果,CompletableFuture 提供了非阻塞的回调式链式调用,还能把多个异步任务自由组合、设置超时与降级。本文用可运行的实战示例,系统讲透它的创建、编排、异常处理与线程池配置,让你在接口聚合、批量调用等场景真正用对、用好 Java 异步编程。
一、为什么需要 CompletableFuture
传统 Future 通过线程池提交任务后,只能调用 get() 阻塞拿结果,一旦任务变多,线程就会卡在等待上,吞吐上不去。更麻烦的是,它不支持任务之间的依赖编排——你没法说「等 A 完成后再用 A 的结果去查 B」。
CompletableFuture 解决了这两件事:一是非阻塞回调,任务完成时自动触发后续逻辑;二是丰富的组合算子(thenCombine / thenCompose / allOf),把一组异步任务拼成有向无环的编排图。它本质上是一个「可完成的未来」,你可以手动 complete,也可以由回调链自动完成。
二、创建异步任务:supplyAsync 与 runAsync
最常用的是 supplyAsync(有返回值)和 runAsync(无返回值,类似 Runnable)。两者都默认提交到 ForkJoinPool.commonPool(),但生产环境强烈建议传入自定义线程池,后文会详细解释。
// 有返回值的异步任务
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> {
try { Thread.sleep(500); } catch (InterruptedException e) {}
return "hello";
});
// 无返回值的异步任务
CompletableFuture<Void> f2 = CompletableFuture.runAsync(() -> {
System.out.println("running in async thread");
});
// 阻塞获取结果(不推荐在请求主线程使用)
String result = f1.get();
注意 get() 是阻塞的,若结果还没算完,调用线程会被挂起。在 Web 请求链路里,应当尽量通过回调链把结果继续往下传,而不是在 Controller 里直接 get()。
三、链式编排:thenApply / thenAccept / thenRun
链式编排是 CompletableFuture 的灵魂。它提供三种典型的「接在任务后面」的方法:
| 方法 | 入参 | 用途 |
|---|---|---|
| thenApply | 有返回值 | 转换结果(map 语义) |
| thenAccept | 消费结果 | 只用结果不返回(消费语义) |
| thenRun | 无入参 | 任务完成后执行一段逻辑 |
CompletableFuture.supplyAsync(() -> fetchOrderId())
.thenApply(orderId -> fetchOrderDetail(orderId)) // 拿到订单ID再查详情
.thenAccept(detail -> System.out.println(detail))
.thenRun(() -> System.out.println("done"));
这三条链默认在同一个线程(或同一个池)里串行推进,结果会自动向下游传递,无需手动 get()。如果某一步很重,可以给它单独传一个 Executor,例如 thenApplyAsync(fn, pool)。
四、组合多个异步任务:thenCompose / thenCombine
当结果依赖「另一个异步任务」时,用 thenCompose 展平嵌套(避免 CompletableFuture<CompletableFuture<T>>);当两个任务相互独立、需要合并结果时,用 thenCombine。
// thenCompose:A 的结果决定 B,展平嵌套
CompletableFuture<String> chained = CompletableFuture
.supplyAsync(() -> userId)
.thenCompose(uid -> CompletableFuture.supplyAsync(() -> queryProfile(uid)));
// thenCombine:A、B 独立,合并两者结果
CompletableFuture<Integer> priceF = CompletableFuture.supplyAsync(() -> priceService.get(id));
CompletableFuture<Integer> stockF = CompletableFuture.supplyAsync(() -> stockService.get(id));
CompletableFuture<String> merged = priceF.thenCombine(stockF,
(price, stock) -> "price=" + price + ",stock=" + stock);
如果只是想「等所有任务完成」而不关心合并,用 CompletableFuture.allOf(f1, f2, f3);只要任意一个完成即可,用 anyOf(...)。allOf 返回 CompletableFuture<Void>,需要再从各个子任务取结果。
五、异常处理:exceptionally 与 handle
异步链路里异常如果不管,会一直透传最终导致 get() 抛出 CompletionException。用 exceptionally 提供降级值,或用 handle 同时拿到结果与异常,做更精细的处理。
CompletableFuture.supplyAsync(() -> riskyCall())
.exceptionally(ex -> {
log.error("call failed", ex);
return "fallback"; // 异常时返回降级结果
});
// handle:无论成功失败都能拿到,常用于统一包装
CompletableFuture<String> safe = CompletableFuture
.supplyAsync(() -> riskyCall())
.handle((v, ex) -> ex != null ? "fallback" : v);
六、超时控制:orTimeout 与 completeOnTimeout
Java 9 起 CompletableFuture 增加了超时原语,不用自己起定时任务就能给异步调用加超时保护:
CompletableFuture<String> withTimeout = CompletableFuture
.supplyAsync(() -> slowCall())
.orTimeout(800, TimeUnit.MILLISECONDS) // 超时抛 TimeoutException
.completeOnTimeout("default", 800, TimeUnit.MILLISECONDS); // 超时给默认值
orTimeout 超时即异常,配合 exceptionally 降级;completeOnTimeout 则直接塞一个默认值,链路不中断。下游接口聚合场景强烈建议每个子任务都加独立超时,防止单个慢调用拖垮整体。
七、线程池:别用默认 ForkJoinPool
默认情况下 supplyAsync 用 ForkJoinPool.commonPool(),而它是所有 CompletableFuture 共享的全局池。一旦某个任务里混入了阻塞 IO,会迅速耗尽公共池线程,连累全应用的异步任务。生产环境务必传入独立的 ExecutorService。
ExecutorService bizPool = new ThreadPoolExecutor(
16, 64, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1024),
new ThreadFactory() {
private final AtomicInteger i = new AtomicInteger();
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "cf-biz-" + i.incrementAndGet());
t.setDaemon(true);
return t;
}
},
new ThreadPoolExecutor.CallerRunsPolicy());
CompletableFuture.supplyAsync(() -> query(id), bizPool);
| 维度 | 默认 commonPool | 自定义线程池 |
|---|---|---|
| 隔离性 | 全局共享,易互相影响 | 业务隔离,互不影响 |
| 线程数 | 核数-1,偏小 | 可按负载调优 |
| 拒绝策略 | 不能配 | 可配CallerRuns等 |
| 可观测性 | 线程名难区分 | 自定义线程名便于排查 |
八、实战:聚合多个下游接口的并发调用
下面这个实战例子聚合三个下游接口(价格、库存、评分),三者无依赖、并发调用,整体超时 1 秒,任一失败都有降级,最后汇总成页面模型。这是电商详情页、聚合网关最常见的用法。
ExecutorService pool = ...;
CompletableFuture<Integer> price = CompletableFuture
.supplyAsync(() -> priceSvc.get(sku), pool)
.completeOnTimeout(-1, 1000, MILLISECONDS);
CompletableFuture<Integer> stock = CompletableFuture
.supplyAsync(() -> stockSvc.get(sku), pool)
.completeOnTimeout(0, 1000, MILLISECONDS);
CompletableFuture<Double> rating = CompletableFuture
.supplyAsync(() -> ratingSvc.get(sku), pool)
.completeOnTimeout(0.0, 1000, MILLISECONDS);
CompletableFuture<Void> all = CompletableFuture.allOf(price, stock, rating);
CompletableFuture<ProductView> view = all.thenApply(v ->
new ProductView(price.join(), stock.join(), rating.join()));
// 在 Web 层用 thenApply 拼装响应,避免主线程阻塞 get
view.thenAccept(pv -> render(pv));
这里每个子任务都用 completeOnTimeout 兜底,即使某个下游超时也只是拿到默认值,整页照常渲染,可用性远高于串行调用或裸 get()。
九、常见坑与最佳实践
把上面的要点收敛成几条可落地的最佳实践:
| 坑 / 实践 | 正确做法 |
|---|---|
| 默认公共池耗尽 | 所有 supplyAsync 都传自定义 Executor |
| 主线程阻塞 get() | 用回调链 thenApply/thenAccept 往下传 |
| 单个慢调用拖垮整体 | 每个子任务加 orTimeout/completeOnTimeout |
| 异常静默丢失 | 统一用 exceptionally/handle 降级并打日志 |
| allOf 后取结果 NPE | 用 join() 取已完成子任务结果,非 get() |
另外,监控上建议给自定义线程池接 Micrometer 指标,观察队列堆积与活跃线程数;排障时结合 CPU 与线程栈分析(可参考我们的线上 CPU 100% 排查实战),快速定位是不是异步任务把池打满。
十、与 Java 21 虚拟线程的关系
Java 21 引入的虚拟线程(Virtual Thread)让「每个任务一个线程」变得廉价,很多人会问:还要不要 CompletableFuture?答案是两者互补。虚拟线程适合大量阻塞式同步代码(直接写 try/finally 更易读);而 CompletableFuture 的声明式编排、组合与超时原语,在需要把多个异步结果拼成复杂 DAG 时依然更简洁。新项目可以「虚拟线程打底 + CompletableFuture 做编排」混合使用。
结语
总结一下:CompletableFuture 是 Java 异步编程与任务编排的基石——用 supplyAsync/runAsync 起步,用 thenApply/thenCompose/thenCombine 编排,用 exceptionally/handle 兜底,用 orTimeout/completeOnTimeout 加超时,并始终记得传入自定义线程池。把这套组合拳用熟,接口的并发能力与健壮性会明显提升。
如果你在搭建 Java 项目的开发环境,推荐用 VS Code 配置 Java 开发环境;交付环节可接 GitHub Actions 自动化构建;涉及缓存降级的场景,Redis 缓存实战 能帮你在异步编排外再加一层速度护城河;最终部署到云上,Docker 容器化部署 是绕不开的一步。




