CompletableFuture 异步编排
用途:把多个下游调用从串行改并行、或组织异步链路时查阅
核心 API 分组
「核心 API 分组」小节链接| 目的 | 方法 |
|---|---|
| 创建 | supplyAsync / runAsync(可传 Executor) |
| 转换 | thenApply / thenAccept / thenRun |
| 串联下一步异步 | thenCompose(扁平化,等价于 monadic bind) |
| 合并不并行结果 | thenCombine、allOf、anyOf |
| 异常处理 | exceptionally、handle、whenComplete |
| 并行分支 | applyToEither(竞速)、allOf 后聚合 |
必须知道的三个坑
「必须知道的三个坑」小节链接- 默认线程池是
ForkJoinPool.commonPool():全 JVM 共享,容量约为核数-1。跑 IO 任务会拖慢所有使用方,且难以隔离。始终显式传入自己的 Executor。 thenApplyAsync与thenApply不同:前者切换线程执行,后者可能由完成者线程直接执行——回调里若做重活会阻塞上游线程。- 异常会被包装成
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