completablefuture-blog.md 4.6 KB

CompletableFuture 进阶:招式和坑,一张表讲完

面向读者:对 Java 并发有一定经验、想快速把 CompletableFuture 用对的工程师。 完整叙述版见 completablefuture-tutorial.md,可运行示例见 src/main/java/space/anyi/Cf*.java

一句话定位

CompletableFuture 是 JDK 8 提供的"异步 + 回调 + 组合"三板斧,本质是把 JS Promise、Stream 的 map/flatMap 思想搬进 Java。它解决的问题:Future 只有"阻塞 get + 单任务",串行/聚合/异常都得自己手撕。

速查表

需求 方法 回调 返回
无返回异步 runAsync(Runnable) Runnable CF<Void>
有返回异步 supplyAsync(Supplier) Supplier CF<T>
无需结果直接过 completedFuture(v) CF<T>
拿上个结果继续加工 thenApply Function CF<U>
消费上个结果 thenAccept Consumer CF<Void>
只等上一环跑完 thenRun Runnable CF<Void>
两任务都完成再合并 thenCombine BiFunction CF<U>
两任务都完成再消费 thenAcceptBoth BiConsumer CF<Void>
两任务都完成等时间点 runAfterBoth Runnable CF<Void>
谁先完成用谁再加工 applyToEither Function CF<U>
谁先完成消费谁 acceptEither Consumer CF<Void>
谁先完成等时间点 runAfterEither Runnable CF<Void>
展平嵌套异步 thenCompose Function→CF CF<U>
全部完成 allOf(...) CF<Void>(各自再 join 汇总)
任一完成 anyOf(...) CF<Object>(需转类型)
失败降级 exceptionally Function CF<T>
成败都观察 whenComplete BiConsumer 不改结果
成败都能处理 handle BiFunction CF<U>
手动定结果 complete / completeExceptionally boolean
强制覆盖 obtrudeValue void(慎用)
取消 cancel(true) boolean

记忆口诀:Apply=加工出结果,Accept=消费不出结果,Run=不管结果只管流程;后缀 Both=Either 只差"都要"还是"谁先"。

五条最佳实践

  1. I/O 一定配独立线程池。 默认 ForkJoinPool.commonPool() 线程数是 CPU核数-1,回调里再嵌 supplyAsync 就会互相等(Thread Starvation),直接把池子卡死。给耗时调用 new ThreadPoolExecutor(...) 并传进 Async 方法。
  2. 组装 BFF / RPC 并发调用用 allOf 一次性把下游都发出去真并行,别用 for + get() 退化成串行。注意 allOf 返回 Void,汇总前再逐个 join()
  3. 回调里禁止 join()/get() 它阻塞的是当前执行线程;在一个 worker 上阻塞等另一个 worker,就是 2 号事故的引信。
  4. 统一异常出口。 想兜底恢复用 exceptionally/handle,只想记录日志用 whenComplete(它不会吞异常,异常仍会沿链传播,最终 join()CompletionException,要用 getCause() 取原始异常)。
  5. 链式只加一次终端操作。 future.thenApply(...).thenApply(...) 是合法的,"上了链就别回头",避免把同一个 Future 反复 join() 造成多次阻塞。

三个高频坑

  • whenComplete 不背锅。 很多同学以为加个 whenComplete 就算异常处理完了,实测它观察完异常后,链下游/join() 依旧抛异常。要"处理掉"请用 handleexceptionally
  • Async 不等于更快。 无 Async 的方法在"触发它的线程"上执行(可能就是主线程)。它提供的不是算力,而是"指定线程池"的能力。
  • 取结果首选 join() get() 抛受检异常,把链式代码弄得很啰嗦;join() 统一包成 CompletionException,链路里不打断。

与隔壁方案的边界

方案 优点 局限
Future 简单、JDK5 就有 只能阻塞取结果,无法链式/聚合
CompletableFuture JDK 内置,聚合强,学习成本低 回调多、线程池管理靠自觉、无背压
响应式流(Reactive/Reactor) 背压、流控、切线程优雅,操作符强大 上手曲线陡,全链路改造成本高

结论:服务内部异步编排,CompletableFuture 是最划算的选择;一旦涉及边缘/流式/背压,再上响应式。别为了"帅"把一个简单聚合演进成全套响应式。

收藏即学会

把速查表截个图存在手机里,遇到"多个异步怎么排"先查它,比背 API 快得多。真正要修炼的,是每写一个回调都问自己:这条链,跑在哪个线程上?会不会饿死公共池?