Просмотр исходного кода

add blog文档:CompletableFuture进阶速查

yangyi 1 неделя назад
Родитель
Сommit
208e4d56da
1 измененных файлов с 64 добавлено и 0 удалено
  1. 64 0
      completablefuture-blog.md

+ 64 - 0
completablefuture-blog.md

@@ -0,0 +1,64 @@
+# 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()` 依旧抛异常。要"处理掉"请用 `handle` 或 `exceptionally`。
+- **`Async` 不等于更快。** 无 Async 的方法在"触发它的线程"上执行(可能就是主线程)。它提供的不是算力,而是"指定线程池"的能力。
+- **取结果首选 `join()`。** `get()` 抛受检异常,把链式代码弄得很啰嗦;`join()` 统一包成 `CompletionException`,链路里不打断。
+
+## 与隔壁方案的边界
+
+| 方案 | 优点 | 局限 |
+| --- | --- | --- |
+| `Future` | 简单、JDK5 就有 | 只能阻塞取结果,无法链式/聚合 |
+| `CompletableFuture` | JDK 内置,聚合强,学习成本低 | 回调多、线程池管理靠自觉、无背压 |
+| 响应式流(Reactive/Reactor) | 背压、流控、切线程优雅,操作符强大 | 上手曲线陡,全链路改造成本高 |
+
+**结论**:服务内部异步编排,`CompletableFuture` 是最划算的选择;一旦涉及边缘/流式/背压,再上响应式。别为了"帅"把一个简单聚合演进成全套响应式。
+
+## 收藏即学会
+
+把速查表截个图存在手机里,遇到"多个异步怎么排"先查它,比背 API 快得多。真正要修炼的,是每写一个回调都问自己:**这条链,跑在哪个线程上?会不会饿死公共池?**