跳转到内容

CompletableFuture 异步编排

用途:把多个下游调用从串行改并行、或组织异步链路时查阅

目的 方法
创建 supplyAsync / runAsync(可传 Executor)
转换 thenApply / thenAccept / thenRun
串联下一步异步 thenCompose(扁平化,等价于 monadic bind)
合并不并行结果 thenCombine、allOf、anyOf
异常处理 exceptionally、handle、whenComplete
并行分支 applyToEither(竞速)、allOf 后聚合
  1. 默认线程池是 ForkJoinPool.commonPool():全 JVM 共享,容量约为核数-1。跑 IO 任务会拖慢所有使用方,且难以隔离。始终显式传入自己的 Executor。
  2. thenApplyAsync 与 thenApply 不同:前者切换线程执行,后者可能由完成者线程直接执行——回调里若做重活会阻塞上游线程。
  3. 异常会被包装成 CompletionException:exceptionally 里要判断 cause;未处理的异常不会打印日志,容易静默失败。
var a = cfA(query1, pool);
var b = cfB(query2, pool);
var c = cfC(query3, pool);
return CompletableFuture.allOf(a, b, c)
.orTimeout(800, MILLISECONDS) // 总预算,而不是各调用相加
.thenApply(v -> assemble(a.join(), b.join(), c.join()))
.exceptionally(ex -> degrade(ex)); // 部分失败时的降级策略

要点:总超时(orTimeout/completeOnTimeout,JDK 9+)而不是每个分支各自超时;部分失败要显式定义降级(缺哪个字段返回默认值,还是整体失败)。

与响应式、虚拟线程的关系

「与响应式、虚拟线程的关系」小节链接
方案 适合 代价
CompletableFuture 有限的并行与编排,同步栈中局部异步 链路深时可读性下降
Reactor / RxJava 流式、背压、大量并发连接 学习曲线与调试成本
虚拟线程(Loom,JDK 21+) 阻塞式代码直接高并发 不能用池化思想管理;仍需要超时与限流

有了虚拟线程后,很多“为了异步而异步”的编排可以退回同步写法,可读性更好;但超时、熔断、并发度控制依然必须显式做。

误区 修正
不传 Executor 统一传入按依赖划分的线程池
用 join() 在业务线程阻塞等待 若必须阻塞,说明该用同步调用 + 虚拟线程
忘记异常处理导致静默失败 链路末端统一 exceptionally 并打日志与指标
每个分支单独设超时 用总预算控制端到端延迟
在回调里再套多层回调(地狱) 用 thenCompose 扁平化或改同步结构
  • 《Java 并发编程实战》第 8 章(异步任务)
  • Richard Warburton《Java 8 并发编程》(CompletableFuture 章节)
  • OpenJDK JDK-21 虚拟线程与结构化并发相关 JEP