Quellcode durchsuchen

docs:blog 扩写实战案例并去除 AI 写作痕迹,更适合发布

yangyi vor 1 Woche
Ursprung
Commit
93009a6c44
1 geänderte Dateien mit 384 neuen und 15 gelöschten Zeilen
  1. 384 15
      completablefuture-blog.md

+ 384 - 15
completablefuture-blog.md

@@ -1,11 +1,11 @@
-# CompletableFuture 进阶:招式和坑,一张表讲完
+# CompletableFuture 实战:招式和坑,一张表讲完
 
 > 面向读者:对 Java 并发有一定经验、想快速把 `CompletableFuture` 用对的工程师。
 > 完整叙述版见 `completablefuture-tutorial.md`,可运行示例见 `src/main/java/space/anyi/Cf*.java`。
 
-## 一句话定位
+先给个定位:`CompletableFuture` 是 JDK 8 加进来的,把异步、回调、组合揉在一起。说白了就是把 JS Promise 和 Stream 的 map/flatMap 搬到了 Java。
 
-`CompletableFuture` 是 JDK 8 提供的"异步 + 回调 + 组合"三板斧,本质是把 JS Promise、Stream 的 map/flatMap 思想搬进 Java。它解决的问题:`Future` 只有"阻塞 get + 单任务",串行/聚合/异常都得自己手撕
+为什么需要它?因为老的 `Future` 只能阻塞 get、只能跑单任务。串行拼接、多任务聚合、异常传递,全得自己手写。`CompletableFuture` 就是为补这些短板来的
 
 ## 速查表
 
@@ -38,22 +38,22 @@
 | 强制覆盖/强制异常 | `obtrudeValue` / `obtrudeException` | — | `void`(慎用) |
 | 取消 | `cancel(true)` | — | `boolean` |
 
-记忆口诀:**Apply=加工出结果,Accept=消费不出结果,Run=不管结果只管流程**;后缀 Both=Either 只差"都要"还是"谁先"。
+记忆口诀: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()` 造成多次阻塞。
-6. **对外异步调用一律配超时。** `orTimeout(2, SECONDS)` 让调用方快速失败(注意它**不会取消底层任务**,只是放弃等待);"过期给默认值也能接受"的场景用 `completeOnTimeout(v, 2, SECONDS)` 更省心。没有超时的异步外呼,等于给自己埋一颗"线程挂死"的雷。
+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()` 造成多次阻塞。
+6. 对外异步调用一律配超时。`orTimeout(2, SECONDS)` 让调用方快速失败(注意它不会取消底层任务,只是放弃等待);"过期给默认值也能接受"的场景用 `completeOnTimeout(v, 2, SECONDS)` 更省心。没有超时的异步外呼,等于给自己埋一颗"线程挂死"的雷。
 
 ## 三个高频坑
 
-- **`whenComplete` 不背锅。** 很多同学以为加个 `whenComplete` 就算异常处理完了,实测它观察完异常后,链下游/`join()` 依旧抛异常。要"处理掉"请用 `handle` 或 `exceptionally`。
-- **`Async` 不等于更快。** 无 Async 的方法在"触发它的线程"上执行(可能就是主线程)。它提供的不是算力,而是"指定线程池"的能力。
-- **取结果首选 `join()`。** `get()` 抛受检异常,把链式代码弄得很啰嗦;`join()` 统一包成 `CompletionException`,链路里不打断。
+`whenComplete` 只是旁观。很多同学以为加个 `whenComplete` 就算异常处理完了,实测它观察完异常后,链下游/`join()` 依旧抛异常。要"处理掉"请用 `handle` 或 `exceptionally`。
+- `Async` 不等于更快。无 Async 的方法在"触发它的线程"上执行(可能就是主线程)。它提供的不是算力,而是"指定线程池"的能力。
+- 取结果首选 `join()`。`get()` 抛受检异常,把链式代码弄得很啰嗦;`join()` 统一包成 `CompletionException`,链路里不打断。
 
 ## 与隔壁方案的边界
 
@@ -63,8 +63,377 @@
 | `CompletableFuture` | JDK 内置,聚合强,学习成本低 | 回调多、线程池管理靠自觉、无背压 |
 | 响应式流(Reactive/Reactor) | 背压、流控、切线程优雅,操作符强大 | 上手曲线陡,全链路改造成本高 |
 
-**结论**:服务内部异步编排,`CompletableFuture` 是最划算的选择;一旦涉及边缘/流式/背压,再上响应式。别为了"帅"把一个简单聚合演进成全套响应式。
+服务内部异步编排,`CompletableFuture` 是最划算的选择。一旦涉及边缘/流式/背压,再上响应式。别为了"帅"把一个简单聚合演进成全套响应式。
+
+## 实战案例:把 API 用到真实场景
+
+下面每个案例都可以直接复制运行,对应 `src/main/java/space/anyi/Cf*.java`。
+
+### 案例一:订单处理流水线(thenApply / thenAccept / thenRun)
+
+典型电商下单流程:创建订单 → 扣库存 → 记日志 → 发通知,每一步都依赖上一步的结果。用 `thenXxx` 串成一条链,代码就像在写流程图:
+
+```java
+CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
+    System.out.println("1. 创建订单");
+    return "订单号 2024001";
+}).thenApply(orderId -> {
+    System.out.println("2. 扣减库存,关联订单: " + orderId);
+    return orderId + " 已扣库存";
+}).thenApply(info -> {
+    System.out.println("3. 记录日志: " + info);
+    return info + " 日志已落库";
+}).thenAccept(result -> {
+    System.out.println("4. 打印流程结果: " + result);   // 只消费结果,无返回值
+}).thenRun(() -> {
+    System.out.println("5. 发送短信通知(不关心数据)");  // 连结果都不关心
+});
+future.join();
+```
+
+这里其实藏着选型逻辑:只要还想继续加工,就用 `thenApply`;结果到此为止,用 `thenAccept` 消费或者 `thenRun` 等个时间点。哪个用得多,取决于你这链条到底有多长。
+
+### 案例二:并发聚合两个独立接口(thenCombine)
+
+页面要同时展示"用户信息"和"订单信息",两个请求互不依赖,谁先回来无所谓,关键是两个都回来后拼在一起:
+
+```java
+CompletableFuture<String> userFuture = CompletableFuture.supplyAsync(() -> {
+    sleep(100);
+    return "用户:李雷";
+});
+CompletableFuture<String> orderFuture = CompletableFuture.supplyAsync(() -> {
+    sleep(200);
+    return "订单:O-10008";
+});
+
+// thenCombine: 等两个都完成,把两个结果合并,返回一个新结果
+CompletableFuture<String> combined = userFuture.thenCombine(orderFuture,
+        (user, order) -> user + " | " + order);
+System.out.println("合并结果: " + combined.join());
+```
+
+这个 API 的网络耗时就是在这里体现的:两个 sleep 是同时进行的,总共约 200ms 而不是 300ms。回调参数是 `(user, order)` 两个结果,这也是 `thenCombine` 和 `thenApply` 的本质区别:前者拿两个,后者拿一个。
+
+### 案例三:缓存优先读取(applyToEither)
+
+数据优先读本地缓存(快),缓存没有就等远程(慢),谁先返回就用谁,避免干等最慢的那一路:
+
+```java
+CompletableFuture<String> cache = CompletableFuture.supplyAsync(() -> {
+    sleep(100);
+    return "缓存数据";
+});
+CompletableFuture<String> remote = CompletableFuture.supplyAsync(() -> {
+    sleep(400);
+    return "远程数据";
+});
+
+// applyToEither: 谁先完成用谁的结果,并继续加工
+CompletableFuture<String> result = cache.applyToEither(remote,
+        data -> "优先使用 -> " + data);
+System.out.println("结果: " + result.join());
+// 输出: 优先使用 -> 缓存数据
+```
+
+`applyToEither` 只关心"谁先来",两个 Future 的角色是对等的,谁先完成就用谁的结果。适合缓存+远程、主备容灾、多数据源"先到先得"。
+
+### 案例四:展平嵌套异步调用(thenCompose)
+
+当 `thenApply` 的回调里返回的又是 `CompletableFuture` 时,会得到嵌套的 `CF<CF<T>>`,取结果得 `join().join()`。`thenCompose` 帮你展平这层嵌套,和 `Stream` 里的 `flatMap` 一模一样:
+
+```java
+CompletableFuture<String> userFuture = CompletableFuture.supplyAsync(() -> {
+    System.out.println("1. 查询用户");
+    return "用户:李雷";
+});
+
+// thenApply 会产生嵌套:CompletableFuture<CompletableFuture<String>>
+CompletableFuture<CompletableFuture<String>> nested =
+        userFuture.thenApply(Cf05_ThenCompose::getOrder);
+System.out.println("两层 join 取结果: " + nested.join().join());
+
+// thenCompose 自动展平,保持链式扁平
+CompletableFuture<String> flat = userFuture.thenCompose(Cf05_ThenCompose::getOrder);
+System.out.println("展平结果: " + flat.join());
+
+// 辅助方法:拿到用户后再异步查他的订单
+private static CompletableFuture<String> getOrder(String user) {
+    return CompletableFuture.supplyAsync(() -> {
+        System.out.println("2. 异步查询 \"" + user + "\" 的订单");
+        return "订单 O-10008";
+    });
+}
+```
+
+`thenApply` 原样透传回调返回值,`thenCompose` 要求回调返回 `CompletionStage` 并帮你展平。前后依赖且每一环都是异步的调用链,用 `thenCompose` 保持扁平。
+
+### 案例五:批量并发聚合(allOf / anyOf)
+
+一个页面需要同时调"基础信息、库存、优惠"三个接口,全部返回后才渲染。别傻傻写三次 `get()`,那会退化成串行:
+
+```java
+CompletableFuture<String> baseInfo = CompletableFuture.supplyAsync(() -> {
+    sleep(100); return "基础信息";
+});
+CompletableFuture<String> stock = CompletableFuture.supplyAsync(() -> {
+    sleep(200); return "库存信息";
+});
+CompletableFuture<String> coupon = CompletableFuture.supplyAsync(() -> {
+    sleep(300); return "优惠信息";
+});
+
+// allOf: 等全部完成(返回 Void),再各自 join 汇总,让三路请求真正并行
+CompletableFuture<Void> all = CompletableFuture.allOf(baseInfo, stock, coupon);
+all.join();
+List<String> data = Arrays.stream(new CompletableFuture[]{baseInfo, stock, coupon})
+        .map(CompletableFuture::join)
+        .map(Object::toString)
+        .collect(Collectors.toList());
+System.out.println("全部就绪,汇总: " + String.join(" | ", data));
+
+// anyOf: 任一路先完成即返回(结果是 Object),用于"谁先回来先给谁出结果"
+CompletableFuture<Object> any = CompletableFuture.anyOf(baseInfo, stock, coupon);
+System.out.println("最先返回: " + any.join());
+```
+
+`allOf` 返回 `CompletableFuture<Void>`,本身不带汇总结果,得在 `join()` 后逐个取。`anyOf` 返回 `CompletableFuture<Object>`,需要做类型转换。这两货名字看着像兄弟,用起来完全是两码事。
+
+### 案例六:异常降级与恢复(exceptionally / whenComplete / handle)
+
+异步任务挂了怎么办?`CompletableFuture` 把异常也当作"一种结果"来处理,三种方式各有所长:
+
+```java
+// exceptionally: 任务失败时给出降级结果(异常被"恢复")
+CompletableFuture<Integer> fallback = failTask().exceptionally(e -> {
+    System.out.println("exceptionally 捕获: " + e.getClass().getSimpleName());
+    return -1;   // 降级返回
+});
+System.out.println("降级结果: " + fallback.join());  // -1
+
+// whenComplete: 只"观察"结果或异常,不消化异常
+CompletableFuture<Integer> observed = failTask();
+try {
+    observed.whenComplete((res, ex) -> {
+        if (ex != null) {
+            System.out.println("whenComplete 观察到异常: " + ex.getClass().getSimpleName());
+        } else {
+            System.out.println("whenComplete 观察到结果: " + res);
+        }
+    }).join();
+} catch (Exception e) {
+    System.out.println("whenComplete 不吞异常, join() 抛: " + e.getClass().getSimpleName());
+}
+
+// handle: 无论成功失败都给一个函数处理,并可返回新结果(可恢复)
+CompletableFuture<String> handled = failTask().handle((res, ex) ->
+        ex == null ? "正常结果: " + res : "出错被恢复: " + ex.getMessage());
+System.out.println("handle 结果: " + handled.join());
+
+private static CompletableFuture<Integer> failTask() {
+    return CompletableFuture.supplyAsync(() -> {
+        throw new RuntimeException("模拟异常");
+    });
+}
+```
+
+三种方式对比:
+
+| 方法 | 成功时触发 | 失败时触发 | 能改变结果 | 会不会吞异常 |
+| --- | --- | --- | --- | --- |
+| `exceptionally` | ✗ | ✓ | ✓ 返回降级值 | 会 |
+| `whenComplete` | ✓ | ✓ | ✗ 只观察 | 不会 |
+| `handle` | ✓ | ✓ | ✓ 可恢复 | 会 |
+
+`whenComplete` 管看不管埋,它只做旁观者。要真正处理异常,请用 `handle` 或 `exceptionally`。还有一点坑:所有异常入口抛的都是 `CompletionException`,`.getCause()` 才是原始异常,别对着 `CompletionException` 猜原因。
+
+### 案例七:手动完成与状态查询(complete / isDone / cancel)
+
+异步世界里有一种情况:结果的给出不由任务决定,而由外部决定。等用户点击、等第三方回调、等消息推送,都可以用空的 `CompletableFuture` 当闸门:
+
+```java
+// 用空 CF 当"闸门",等外部事件触发后手动 complete
+CompletableFuture<String> gate = new CompletableFuture<>();
+new Thread(() -> {
+    sleep(500);
+    gate.complete("外部线程触发了完成");
+}).start();
+System.out.println("阻塞等待手动完成: " + gate.join());
+
+// completeExceptionally: 手动让任务以异常结束,常用于"超时兜底"
+CompletableFuture<Integer> timeout = new CompletableFuture<>();
+timeout.completeExceptionally(new RuntimeException("调用超时,兜底结束"));
+try {
+    timeout.join();
+} catch (Exception e) {
+    System.out.println("completeExceptionally 抛给调用方: " + e.getCause());
+}
+
+// isDone: 查询任务是否已结束(成功、失败、取消都算 done)
+CompletableFuture<String> pending = new CompletableFuture<>();
+System.out.println("刚创建未完成 isDone: " + pending.isDone());  // false
+pending.complete("已赋值");
+System.out.println("完成后 isDone: " + pending.isDone());        // true
+
+// cancel: 取消任务
+CompletableFuture<String> cancelling = new CompletableFuture<>();
+System.out.println("cancel() 返回值: " + cancelling.cancel(true));
+System.out.println("取消后 isCancelled: " + cancelling.isCancelled()
+        + ", isCompletedExceptionally: " + cancelling.isCompletedExceptionally());
+```
+
+`new CompletableFuture<>()` 当栅栏/闸门,等外部事件到来后再 `complete`,让等待方放行。`obtrudeValue` / `obtrudeException` 不管状态强制覆盖,一般不要在生产乱用。
+
+顺带说一句,`isDone` 不等于"成功"。取消、异常、正常完成都算 done,判断成败要看 `isCompletedExceptionally` 或直接 `join()` 接异常。
+
+### 案例八:自定义线程池(Async 变体与线程分配)
+
+所有 `xxxAsync` 方法不指定线程池时,都跑在 `ForkJoinPool.commonPool()` 上。这是最容易踩的坑:
+
+```java
+// 不指定线程池时,默认全部任务都跑在 commonPool 上
+CompletableFuture<String> defaultOne = CompletableFuture.supplyAsync(() -> {
+    System.out.println("默认线程池: " + Thread.currentThread().getName());
+    return "OK";
+});
+defaultOne.join();
+
+// 重活建议显式指定自定义线程池,避免抢占公共池
+ExecutorService pool = Executors.newFixedThreadPool(4);
+CompletableFuture<String> custom = CompletableFuture.supplyAsync(() -> {
+    System.out.println("自定义池 supplyAsync: " + Thread.currentThread().getName());
+    return "结果";
+}, pool).thenApplyAsync(r -> {
+    System.out.println("自定义池 thenApplyAsync: " + Thread.currentThread().getName());
+    return r + " 加工完成";
+}, pool);
+System.out.println("最终结果: " + custom.join());
+pool.shutdown();
+
+// 关键区别: 不带 Async 后缀的方法,在"触发者"所在的线程执行
+CompletableFuture<String> noAsync = CompletableFuture.supplyAsync(() -> {
+    System.out.println("supplyAsync        : " + Thread.currentThread().getName());
+    return "r";
+}).thenApply(r -> {
+    System.out.println("thenApply(无Async) : " + Thread.currentThread().getName());
+    return r;
+}).thenApplyAsync(r -> {
+    System.out.println("thenApplyAsync(无池): " + Thread.currentThread().getName());
+    return r;
+});
+noAsync.join();
+```
+
+典型输出:
+
+```
+默认线程池: ForkJoinPool.commonPool-worker-1
+自定义池 supplyAsync: pool-1-thread-1
+自定义池 thenApplyAsync: pool-1-thread-2
+supplyAsync        : ForkJoinPool.commonPool-worker-1
+thenApply(无Async) : space.anyi.Cf09_CustomExecutor.main()
+thenApplyAsync(无池): ForkJoinPool.commonPool-worker-1
+```
+
+`commonPool` 线程数 = CPU核数 - 1,I/O 阻塞任务会把它饿死(Thread Starvation)。不带 `Async` 的方法在"触发线程"上执行,带 `Async` 且不传池子才提交到 `commonPool`。遇到 I/O 阻塞、长耗时任务,务必 `new ThreadPoolExecutor(...)` 并传给带 `Async` 的方法。
+
+### 案例九:超时控制与异步降级(orTimeout / completeOnTimeout / exceptionallyCompose)
+
+异步调用最怕挂死:下游接口慢、网络闪断,调用方还在傻等。生产里一切异步外呼都必须有超时兜底:
+
+```java
+// orTimeout: 限时等待,超时后以 TimeoutException 失败
+// 注意:它不会取消底层任务,慢任务仍会继续执行
+CompletableFuture<String> slow = CompletableFuture.supplyAsync(() -> {
+    sleep(1500);
+    System.out.println("底层慢任务执行完毕(并未被 orTimeout 取消)");
+    return "慢任务结果";
+});
+try {
+    slow.orTimeout(500, TimeUnit.MILLISECONDS).join();
+} catch (Exception e) {
+    System.out.println("orTimeout 超时失败: " + e.getCause());
+}
+
+// completeOnTimeout: 超时就给默认值完成,不抛异常
+CompletableFuture<String> withDefault = CompletableFuture.supplyAsync(() -> {
+    sleep(1500);
+    return "真实数据";
+}).completeOnTimeout("超时兜底数据", 500, TimeUnit.MILLISECONDS);
+System.out.println("completeOnTimeout 结果: " + withDefault.join());
+// 输出: 超时兜底数据
+
+// exceptionallyCompose: 异常后拼接一个异步降级任务,结果自动展平
+CompletableFuture<String> recovered = CompletableFuture.supplyAsync(() -> {
+    throw new RuntimeException("主任务失败");
+}).exceptionallyCompose(ex ->
+        CompletableFuture.supplyAsync(() -> "降级方案返回的数据"));
+System.out.println("exceptionallyCompose 降级结果: " + recovered.join());
+```
+
+`orTimeout` 让调用方快速失败,适合"拿不到就报错让上层重试/降级"的业务。`completeOnTimeout` 让调用方拿到兜底值,适合"过期用默认数据也行"的业务(如缓存兜底)。`exceptionallyCompose` 补上了异常处理谱系的最后一块拼图:
+
+| 能力 | 正常路径 | 异常路径 |
+| --- | --- | --- |
+| 同步续接 | `thenApply` | `exceptionally` |
+| 异步续接 | `thenCompose` | `exceptionallyCompose` |
+| 观察兜底 | `whenComplete` / `handle` | — |
+
+## 综合实战:一个完整的异步编排场景
+
+把上面的 API 组合起来,写一个真实场景:电商商品详情页需要并发调用 5 个下游服务,拿到数据后组装成页面 DTO,任何一路失败都给降级默认值,整体有 3 秒超时。
+
+```java
+public class ProductDetailPage {
+    private final ExecutorService pool = Executors.newFixedThreadPool(8);
+
+    public CompletableFuture<ProductDTO> loadDetail(String productId) {
+        // 五个下游服务并发调用
+        CompletableFuture<ProductInfo> infoFuture = CompletableFuture
+                .supplyAsync(() -> remoteCall("商品基础信息", 200), pool)
+                .exceptionally(ex -> new ProductInfo("默认商品", 0.0));
+
+        CompletableFuture<StockInfo> stockFuture = CompletableFuture
+                .supplyAsync(() -> remoteCall("库存信息", 150), pool)
+                .exceptionally(ex -> new StockInfo(0));
+
+        CompletableFuture<CouponInfo> couponFuture = CompletableFuture
+                .supplyAsync(() -> remoteCall("优惠信息", 300), pool)
+                .exceptionally(ex -> new CouponInfo("无可用优惠"));
+
+        CompletableFuture<SellerInfo> sellerFuture = CompletableFuture
+                .supplyAsync(() -> remoteCall("商家信息", 100), pool)
+                .exceptionally(ex -> new SellerInfo("未知商家"));
+
+        CompletableFuture<List<Comment>> commentFuture = CompletableFuture
+                .supplyAsync(() -> remoteCall("评论列表", 250), pool)
+                .exceptionally(ex -> Collections.emptyList());
+
+        // allOf 等全部完成,再各自 join 汇总
+        CompletableFuture<Void> all = CompletableFuture.allOf(
+                infoFuture, stockFuture, couponFuture, sellerFuture, commentFuture);
+
+        // 整体 3 秒超时
+        return all.orTimeout(3, TimeUnit.SECONDS).thenApply(v -> {
+            ProductInfo info = infoFuture.join();
+            StockInfo stock = stockFuture.join();
+            CouponInfo coupon = couponFuture.join();
+            SellerInfo seller = sellerFuture.join();
+            List<Comment> comments = commentFuture.join();
+            return new ProductDTO(info, stock, coupon, seller, comments);
+        });
+    }
+
+    private <T> T remoteCall(String name, long ms) {
+        // 模拟远程调用
+        sleep(ms);
+        return (T) name + " 数据";
+    }
+}
+```
+
+这个例子把前面的 API 串起来了:`supplyAsync` + 自定义线程池(案例八)、`exceptionally` 降级(案例六)、`allOf` 批量并发聚合(案例五)、`orTimeout` 超时快速失败(案例九)、`thenApply` 结果组装(案例一)。
 
 ## 收藏即学会
 
-把速查表截个图存在手机里,遇到"多个异步怎么排"先查它,比背 API 快得多。真正要修炼的,是每写一个回调都问自己:**这条链,跑在哪个线程上?会不会饿死公共池?**
+把速查表截个图存在手机里,遇到"多个异步怎么排"先查它,比背 API 快得多。真正要修炼的,是每写一个回调都问自己:这条链,跑在哪个线程上?会不会饿死公共池?