← 返回题目列表

CompletableFuture 如何编排异步任务?常见陷阱有哪些?

高频 困难 第 15 / 24 题 更新于 2026/07/26
CompletableFuture异步编排线程池异常处理

简化版

CompletableFuture 用 CompletionStage 描述异步任务的依赖关系:转换结果用 thenApply,依赖另一个异步任务用 thenCompose,合并独立任务用 thenCombine。无后缀阶段不保证切换线程,Async 方法未指定 Executor 时通常使用 ForkJoinPool.commonPool(),生产代码应按任务类型显式治理线程池、异常、超时和取消。

详细版

runAsync 执行无返回值任务,supplyAsync 产生结果。串行转换用 thenApply;回调返回 CompletionStage 时用 thenCompose 展平嵌套;两个互不依赖的结果用 thenCombine,等待一组任务可用 allOf,但 allOf 自身不收集各任务结果。

exceptionally 只在异常时提供替代值,handle 无论成功失败都把结果转换成新值,whenComplete 更适合观察结果或记录日志而不改变正常结果。join() 把失败包装为未检查的 CompletionExceptionget() 则声明 InterruptedExceptionExecutionException

完整版教学

一、先画出依赖关系

假设用户和商品可以并行查询,而推荐结果依赖两者,可以这样表达:

CompletableFuture<User> userFuture =
        CompletableFuture.supplyAsync(() -> loadUser(id), ioExecutor);
CompletableFuture<List<Product>> productFuture =
        CompletableFuture.supplyAsync(this::loadProducts, ioExecutor);

CompletableFuture<Result> resultFuture = userFuture.thenCombine(
        productFuture,
        this::recommend
);

先分清任务是串行依赖还是彼此独立,比机械记忆 API 更重要。错误地先 join() 第一个任务再创建第二个任务,会把本可并行的调用重新串行化。

二、thenApply 与 thenCompose

thenApply 接收上一步结果并同步计算普通值,类似 Optional 的 map。如果下一步本身返回 CompletableFuture,使用 thenApply 会产生嵌套:

CompletableFuture<CompletableFuture<Order>> nested =
        userFuture.thenApply(user -> loadOrderAsync(user.id()));

CompletableFuture<Order> flat =
        userFuture.thenCompose(user -> loadOrderAsync(user.id()));

thenCompose 类似 flatMap,表达“下一阶段依赖上一阶段,并且下一阶段本身也是异步的”。

三、线程到底在哪里执行

不带 Async 的依赖阶段,可能由完成前一阶段的线程执行;如果前一阶段已经完成,也可能由注册阶段的调用线程执行,因此不能假定固定线程。带 Async 但不传 Executor 的方法,默认异步执行设施通常是 ForkJoinPool.commonPool()

阻塞数据库或 HTTP 调用直接占用 common pool,可能拖慢同进程中无关的异步任务和并行流。应根据 CPU 计算、阻塞 I/O 和下游容量选择并显式传入 Executor,同时配置队列、拒绝策略、监控和上下文传递。线程池越大也不等于下游承载力越高。

四、异常处理方法的区别

CompletableFuture<User> safe = userFuture
        .whenComplete((user, error) -> metrics.record(error))
        .exceptionally(error -> fallbackUser());

whenComplete 能同时观察结果与异常,正常情况下保留原结果;如果回调本身抛异常,最终异常规则还需谨慎处理。exceptionally 只在上游异常完成时恢复为同类型结果。handle 总会执行并返回一个新结果,适合把成功和失败统一映射为业务响应。

异常往往包在 CompletionExceptionExecutionException 中,记录和分类时要检查 cause。不要在链尾完全忽略返回的 Future,否则异常可能长期无人观察。

五、超时、取消与 allOf

现代 JDK 可用 orTimeout 让阶段超时异常完成,或用 completeOnTimeout 提供超时默认值。但让 Future 超时完成,不代表底层网络请求一定被物理中止;仍需给 HTTP、数据库等客户端设置自己的连接和读取超时。

cancel 也不会自动形成完整的结构化取消传播,组合链中的底层任务和兄弟任务是否停止需要单独设计。allOf(f1, f2) 只返回 CompletableFuture<Void>,成功后仍要从各 Future 获取结果;只要其中一个异常,组合结果也会异常完成,但其他任务不因此自动取消。

六、避免用异步外壳包住同步阻塞

调用 join() 会阻塞当前线程。如果业务每一步创建 Future 后立刻 join(),就没有获得异步组合的并发价值,只增加了包装和排错成本。应尽量把 Future 一直组合到系统边界,再由真正需要同步结果的位置等待。

对于大量采用顺序阻塞风格的 I/O 任务,Java 21 虚拟线程可能比复杂 CompletableFuture 链更易读;CompletableFuture 仍适合表达明确的数据流依赖和非阻塞 API 组合,两者应按代码模型选择。

七、用数字分析并行收益与线程池压力

假设查询用户和订单各需 200 ms,且二者互不依赖。先查用户再查订单的理论等待约 400 ms;同时启动后再 thenCombine,理想关键路径约为 max(200, 200) = 200 ms,再加调度和合并开销。若订单必须依赖用户 ID,就不能为了追求数字而伪造并行,应使用 thenCompose 表达真实依赖。

串行:user 200 ms → order 200 ms → combine,约 400 ms
并行:user 200 ms ┐
                  ├→ combine,约 200 ms + 开销
      order 200 ms┘

如果下游数据库连接池只有 50 个连接,却给 I/O Executor 配 500 个线程,同时到来的 500 个任务仍只有约 50 个能真正查询,其余线程只是在等待连接。Future 编排不能替代容量控制,线程池、队列、客户端超时和下游并发上限必须共同设计。

关系合适方法返回形态是否天然并行
普通值转换thenApplyU
下一步返回 FuturethenCompose展平为 Future<U>依赖上一步
两个独立结果汇合thenCombine合并值前提是先独立启动
等待一组完成allOfFuture<Void>不负责收集结果

记忆钩子:先画依赖图,再选 API;先定义资源上限,再选 Executor。API 名字不能把串行依赖变成并行,也不能把 50 个数据库连接变成 500 个。

八、常见误区与追问

  • 误区:所有 thenXxx 回调都会自动切换到新线程。 无 Async 后缀的阶段可能由完成上游的线程或其他完成调用者执行,不能假定固定线程。
  • 误区:带 Async 就一定使用业务专属线程池。 未传 Executor 时通常使用 common pool,可能与并行流和其他异步任务互相影响。
  • 误区:orTimeout 会自动中止底层 HTTP 或数据库请求。 它让 CompletableFuture 超时异常完成,底层操作仍需客户端超时与取消机制。
  • 误区:cancel(true) 一定会中断正在执行的底层任务。 CompletableFuture 不直接控制底层计算,取消传播和资源清理必须按任务实现设计。
  • 追问:join()get() 的异常有什么区别? join 抛未检查的 CompletionException,get 声明 InterruptedExceptionExecutionException
  • 追问:allOf 如何收集结果? 它只表示全部完成,成功后仍需从各个 future 读取结果,并决定任一失败时的聚合策略。
  • 追问:何时选虚拟线程而不是 CompletableFuture? 大量顺序阻塞 I/O 更看重可读性时可评估虚拟线程;明确的数据流依赖和已有异步 API 仍适合 CompletionStage。

九、加强记忆

普通值转换用 thenApply,异步依赖展平用 thenCompose,独立结果汇合用 thenCombine。牢记无后缀方法不承诺线程,未指定 Executor 的 Async 通常走 common pool;异常恢复、超时完成和 Future 取消也不等于底层任务自动停止。