面向读者:对 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 只差"都要"还是"谁先"。
ForkJoinPool.commonPool() 线程数是 CPU核数-1,回调里再嵌 supplyAsync 就会互相等(Thread Starvation),直接把池子卡死。给耗时调用 new ThreadPoolExecutor(...) 并传进 Async 方法。allOf。 一次性把下游都发出去真并行,别用 for + get() 退化成串行。注意 allOf 返回 Void,汇总前再逐个 join()。join()/get()。 它阻塞的是当前执行线程;在一个 worker 上阻塞等另一个 worker,就是 2 号事故的引信。exceptionally/handle,只想记录日志用 whenComplete(它不会吞异常,异常仍会沿链传播,最终 join() 抛 CompletionException,要用 getCause() 取原始异常)。future.thenApply(...).thenApply(...) 是合法的,"上了链就别回头",避免把同一个 Future 反复 join() 造成多次阻塞。whenComplete 不背锅。 很多同学以为加个 whenComplete 就算异常处理完了,实测它观察完异常后,链下游/join() 依旧抛异常。要"处理掉"请用 handle 或 exceptionally。Async 不等于更快。 无 Async 的方法在"触发它的线程"上执行(可能就是主线程)。它提供的不是算力,而是"指定线程池"的能力。join()。 get() 抛受检异常,把链式代码弄得很啰嗦;join() 统一包成 CompletionException,链路里不打断。| 方案 | 优点 | 局限 |
|---|---|---|
Future |
简单、JDK5 就有 | 只能阻塞取结果,无法链式/聚合 |
CompletableFuture |
JDK 内置,聚合强,学习成本低 | 回调多、线程池管理靠自觉、无背压 |
| 响应式流(Reactive/Reactor) | 背压、流控、切线程优雅,操作符强大 | 上手曲线陡,全链路改造成本高 |
结论:服务内部异步编排,CompletableFuture 是最划算的选择;一旦涉及边缘/流式/背压,再上响应式。别为了"帅"把一个简单聚合演进成全套响应式。
把速查表截个图存在手机里,遇到"多个异步怎么排"先查它,比背 API 快得多。真正要修炼的,是每写一个回调都问自己:这条链,跑在哪个线程上?会不会饿死公共池?