# JDK CompletableFuture API 使用教程 > 本文所有代码均为自包含、可直接运行的 `main()` 方法,对应 `src/main/java/space/anyi/Cf*.java` 文件。 > 建议边读边跑:`mvn exec:java -Dexec.mainClass=space.anyi.Cf01_CreateStage` 还记得我们在 JUC 阶段认识的 `Future` 吗?它是 JDK 5 时代异步任务的代表: ```java ExecutorService pool = Executors.newFixedThreadPool(4); Future future = pool.submit(() -> "结果"); String result = future.get(); // 阻塞!拿不到就先卡在这里 ``` 用起来总觉得不够爽,痛点相当明显: 1. **`get()` 是阻塞的**:想拿结果只能干等,要么就自己轮询 `isDone()`,无法"结果好了再通知我"。 2. **串不起任务**:做完"查用户",想接着异步"查订单",`Future` 做不到,只能再 submit 一次、把上一个结果手动传进去。 3. **劝不动多个任务**:并发调三个接口想一起汇总,`Future` 只能一个个 get,三个请求等于被"拉直"成串行。 4. **异常难处理**:只能 `try-catch` 包住 `get()`,异常传递全靠一层层往外抛。 于是 JDK 8 给 Java 带来了一个对标前端 Promise 思想的神器——`CompletableFuture`。它把"异步 + 回调 + 组合"揉在一个类里,让你像写流程图一样描述异步逻辑。这一系列,我们就来把它吃透。 *** ## 一、创建异步任务:runAsync / supplyAsync / completedFuture 先从一个问题开始:我要在另一个线程里干点活儿,怎么把任务"交出去"? `CompletableFuture` 提供了三个入口(详见 `Cf01_CreateStage`): ```java public static void main(String[] args) throws Exception { // runAsync:没有返回值的异步任务,相当于"让个线程去跑一段代码" CompletableFuture voidFuture = CompletableFuture.runAsync(() -> { System.out.println("runAsync 正在执行: " + Thread.currentThread().getName()); }); voidFuture.join(); // 阻塞等待任务完成 // supplyAsync:有返回值的异步任务,这是最常用的入口 CompletableFuture stringFuture = CompletableFuture.supplyAsync(() -> { System.out.println("supplyAsync 正在执行: " + Thread.currentThread().getName()); return "Hello CompletableFuture"; }); System.out.println("supplyAsync 拿到的结果: " + stringFuture.get()); // completedFuture:直接得到一个已经完成的任务,无需起线程 CompletableFuture done = CompletableFuture.completedFuture(42); System.out.println("completedFuture 的结果: " + done.getNow(-1)); // getNow:任务已完成就返回结果,未完成不阻塞,直接返回默认值 CompletableFuture slowTask = CompletableFuture.supplyAsync(() -> { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } return "完成了"; }); System.out.println("任务还没完成 getNow 返回: " + slowTask.getNow("默认值")); System.out.println("阻塞等待任务结束得到: " + slowTask.join()); } ``` 拿到结果有四种姿势,别一次全用,按需选择一个即可: | 方法 | 行为 | 什么时候用 | | --- | --- | --- | | `get()` | 阻塞等待,结果以受检异常方式抛出 | 方法签名本就允许抛异常时 | | `join()` | 阻塞等待,把异常包装成 `CompletionException` 扔出 | 日常首选,不强制 try-catch | | `getNow(默认值)` | 已完成返回结果,未完成立即返回默认值,绝不阻塞 | 想"有就有、没有算了" | | `get(超时, 单位)` | 等待限定时间,超时抛 `TimeoutException` | 给外部调用设置超时上限 | > `join()` 和 `get()` 都是阻塞方法,阻塞的是"调用它们的线程"。 > 学习阶段无妨,但在生产环境的回调里**不要**随便调用,否则会反向堵塞线程池(详见第九章)。 **使用场景:** - `runAsync`:发短信、写异步日志、触发一个不用回执的后台任务。 - `supplyAsync`:把耗时的 RPC/IO 丢到后台,主线程先去干别的,结果好了再取。 - `completedFuture`:在拼接处理链时,给"已知结果"一个统一的起点。 *** ## 二、串行处理:thenApply / thenAccept / thenRun 我们已经能做到"异步开跑",但现在关心另一个问题:**上一个任务的结果,能不能直接喂给下一个任务?** 还记得 JavaSE 里学多线程时的"排序加 sleep"吗?那种靠 `Thread` + 标志位手撕回调的方式,在这里可以用一条链优雅搞定(详见 `Cf02_ThenSeries`): ```java public static void main(String[] args) throws Exception { CompletableFuture 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 / thenAcceptBoth / runAfterBoth 串行解决了"先后依赖",但现实里更多是**两个互不依赖的任务同时干,最后凑一块儿**。比如页面上要同时展示"用户信息"和"订单信息",分两次异步请求发出,谁先回来无所谓,关键是两个都回来后拼在一起展示(详见 `Cf03_ThenCombine`): ```java public static void main(String[] args) throws Exception { CompletableFuture userFuture = CompletableFuture.supplyAsync(() -> { sleep(100); return "用户:李雷"; }); CompletableFuture orderFuture = CompletableFuture.supplyAsync(() -> { sleep(200); return "订单:O-10008"; }); // thenCombine: 等两个都完成,把两个结果合并,返回一个新结果 CompletableFuture combined = userFuture.thenCombine(orderFuture, (user, order) -> user + " | " + order); System.out.println("thenCombine 合并结果: " + combined.join()); // thenAcceptBoth: 等两个都完成,消费两个结果,但无返回值 userFuture.thenAcceptBoth(orderFuture, (user, order) -> System.out.println("thenAcceptBoth 同时拿到: " + user + " & " + order)).join(); // runAfterBoth: 等两个都完成,不关心结果,只等一个时间点 userFuture.runAfterBoth(orderFuture, () -> System.out.println("runAfterBoth: 两路结果都就绪")).join(); } ``` > 注意看 `thenCombine` 的回调参数 `(user, order)` —— 它和 `thenApply` 的关键区别就在于:**回调能不能拿到两个任务的结果**。简单记:`thenApply(BiFunction)` 永远只接收**一个** Task 的结果,所以它是 Function 不是 BiFunction;而 `thenCombine` 接收的是**两个**。 **使用场景:** - `thenCombine`:并发拉取两个接口,合并字段后返回给前端。 - `thenAcceptBoth`:两个异步结果都完成后触发一个副作用(入库、发通知)。 - `runAfterBoth`:做"两个条件都满足才能继续"的闸门,比如数据校验 + 权限校验都通过才放行。 *** ## 四、二选一:applyToEither / acceptEither / runAfterEither 与"两个都要"相对的,是"两个谁先来都行"。典型场景:**读数据先走本地缓存(快),缓存没有就等远程(慢),谁先返回用谁**,避免干等最慢那一路(详见 `Cf04_ThenEither`): ```java public static void main(String[] args) throws Exception { CompletableFuture cache = CompletableFuture.supplyAsync(() -> { sleep(100); return "缓存数据"; }); CompletableFuture remote = CompletableFuture.supplyAsync(() -> { sleep(400); return "远程数据"; }); // applyToEither: 谁先完成用谁的结果,并继续用指定函数加工 CompletableFuture result = cache.applyToEither(remote, data -> "优先使用 -> " + data); System.out.println("applyToEither 结果: " + result.join()); // acceptEither: 谁先完成消费谁的结果,无返回值 CompletableFuture a = CompletableFuture.supplyAsync(() -> { sleep(50); return "A"; }); CompletableFuture b = CompletableFuture.supplyAsync(() -> { sleep(400); return "B"; }); a.acceptEither(b, s -> System.out.println("acceptEither 选中了先完成的: " + s)).join(); // runAfterEither: 谁先完成都行,等时间点即可,不关心数据 CompletableFuture r1 = CompletableFuture.runAsync(() -> sleep(60)); CompletableFuture r2 = CompletableFuture.runAsync(() -> sleep(500)); r1.runAfterEither(r2, () -> System.out.println("runAfterEither: 有一路先完成了")).join(); r2.join(); // 等待较慢的一路也结束,避免 main 退出时打断它 } ``` 上一个示例输出 `缓存数据` 为主,因为线程调度下 `cache`(sleep 100ms)一定先完成。 **使用场景:** 缓存与远程的双通道读取、主备服务的容灾切换、多数据源"先到先得"。 *** ## 五、展平组合:thenCompose 看到这里,你的脑中可能冒出一个问题:如果 `thenApply` 的回调里,返回的又是一个 `CompletableFuture`,会怎样? 结果就是嵌套:`CompletableFuture>`。要取最里层真的结果,得 `join().join()`,层级一深就开始"回调地狱"了。此时应该换用 `thenCompose`(详见 `Cf05_ThenCompose`): ```java public static void main(String[] args) throws Exception { CompletableFuture userFuture = CompletableFuture.supplyAsync(() -> { System.out.println("1. 查询用户"); return "用户:李雷"; }); // 使用 thenApply,返回嵌套结构,需要两层 join 才能取到结果 CompletableFuture> nested = userFuture.thenApply(Cf05_ThenCompose::getOrder); System.out.println("thenApply 拼出嵌套: " + nested.getClass().getSimpleName()); System.out.println(" 两层 join 取结果: " + nested.join().join()); // thenCompose 会自动展平内层 Future,链式调用只有一个层级 CompletableFuture flat = userFuture.thenCompose(Cf05_ThenCompose::getOrder); System.out.println("thenCompose 展平结果: " + flat.join()); } private static CompletableFuture getOrder(String user) { return CompletableFuture.supplyAsync(() -> { System.out.println("2. 异步查询 \"" + user + "\" 的订单"); return "订单 O-10008"; }); } ``` > `thenCompose` 和 `thenApply` 长得像,但本质不同: > > - `thenApply` 回调返回普通值或 `CompletableFuture`,**原样透传**,你给的什么它就是什么。 > - `thenCompose` 要求回调返回 `CompletionStage`,并帮你把这一层**展平**。 > > 这和 `Stream` 里 `map` 与 `flatMap` 的关系一模一样,可以类比记忆。 **使用场景:** 前后依赖、且每一环都是异步的调用链,比如"根据用户ID异步查用户 → 再根据用户异步查其订单 → 再查订单中的商品",贯穿三层以上时 `thenCompose` 让代码保持扁平。 *** ## 六、批量聚合:allOf / anyOf 单个任务好办,如果是一**批**任务呢?比如某页面需要同时调"基础信息、库存、优惠"三个接口,全部返回后一起渲染。这时别傻傻写三次 `get()`——那会退化成串行,应该用 `allOf` / `anyOf`(详见 `Cf06_AllOfAnyOf`): ```java public static void main(String[] args) throws Exception { CompletableFuture baseInfo = CompletableFuture.supplyAsync(() -> { sleep(100); return "基础信息"; }); CompletableFuture stock = CompletableFuture.supplyAsync(() -> { sleep(200); return "库存信息"; }); CompletableFuture coupon = CompletableFuture.supplyAsync(() -> { sleep(300); return "优惠信息"; }); // allOf: 等全部完成(返回 Void),再各自 join 汇总,让三路请求真正并行 CompletableFuture all = CompletableFuture.allOf(baseInfo, stock, coupon); all.join(); List data = Arrays.stream(new CompletableFuture[]{baseInfo, stock, coupon}) .map(CompletableFuture::join) .map(Object::toString) .collect(Collectors.toList()); System.out.println("allOf 全部就绪,汇总结果: " + String.join(" | ", data)); // anyOf: 任一路先完成即返回(结果是 Object),用于"谁先回来先给谁出结果" CompletableFuture any = CompletableFuture.anyOf(baseInfo, stock, coupon); System.out.println("anyOf 最先返回的一路: " + any.join()); } ``` > 一个很容易掉的坑:`allOf` 返回的是 `CompletableFuture`,本身**不带任何汇总结果**。 > 想拿每个任务的结果,得在 `allOf().join()` 之后再对每个 Future 分别 `join()`。 > 但注意,此时它们都已经完成了,再 join 只是"取数据",不会再等。 > > `anyOf` 返回类型是 `CompletableFuture`,结果要做类型判断/强转回自己的类型。 **使用场景:** - `allOf`:BFF 层并发聚合多个下游接口、批量文件下载后统一处理、多副本校验。 - `anyOf`:多个候选源"谁先出结果就用谁"、健康检查里"只要一路通就视为可用"。 *** ## 七、异常处理:exceptionally / whenComplete / handle 异步任务挂了怎么办?`CompletableFuture` 把异常也当作"一种结果"来处理,等着我们用回调接住(详见 `Cf07_ExceptionHandling`): ```java public static void main(String[] args) throws Exception { // exceptionally: 任务失败时给出降级结果(类似异常被"恢复") CompletableFuture fallback = failTask().exceptionally(e -> { System.out.println("exceptionally 捕获: " + e.getClass().getSimpleName()); return -1; // 降级返回 }); System.out.println("exceptionally 降级结果: " + fallback.join()); // whenComplete: 只"观察"结果或异常,做了日志记录就够, // 注意它不消化异常 —— 原任务失败,链上的 join/get 依然会抛 CompletionException CompletableFuture 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 handled = failTask().handle((res, ex) -> ex == null ? "正常结果: " + res : "出错被恢复: " + ex.getMessage()); System.out.println("handle 结果: " + handled.join()); } ``` 三个方法放一起对比,区别一目了然: | 方法 | 成功时触发? | 失败时触发? | 能拿到异常? | 能改变结果? | 会不会吞掉异常 | | --- | --- | --- | --- | --- | --- | | `exceptionally` | ✗(成功就跳过) | ✓ | ✓ | ✓ 返回降级值 | 会 | | `whenComplete` | ✓ | ✓ | ✓ | ✗ 只观察 | 不会 | | `handle` | ✓ | ✓ | ✓ | ✓ 可恢复 | 会 | > 关键记忆点:回调里出现异常时,**所有完成入口抛的都是 `CompletionException`,`.getCause()` 才是原始异常**。 **使用场景:** - `exceptionally`:调用外部服务失败后返回兜底默认值,或记录并返回空数据。 - `whenComplete`:收尾日志、打监控埋点,看看"这任务到底成了还是挂了"。 - `handle`:最灵活,"成功就取结果、失败就换方案",适合做降级/默认值策略。 *** ## 八、手动完成:complete / completeExceptionally / obtrudeValue / cancel 前面聊的都是"任务自己往结束跑",但异步世界里还有一种情况:**结果的给出,不由任务决定,而由外部决定**。比如某个阶段要等用户点击、等外部回调、等一个守护线程汇报(详见 `Cf08_ManualComplete`): ```java public static void main(String[] args) throws Exception { // complete: 任务的完成时机由我们手动决定(比如等待外部事件/用户操作) CompletableFuture gate = new CompletableFuture<>(); new Thread(() -> { try { Thread.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); } gate.complete("外部线程触发了完成"); }).start(); System.out.println("阻塞等待手动完成: " + gate.join()); // completeExceptionally: 手动让任务以异常结束,常用于"超时兜底" CompletableFuture timeout = new CompletableFuture<>(); timeout.completeExceptionally(new RuntimeException("调用超时,兜底结束")); try { timeout.join(); } catch (Exception e) { System.out.println("completeExceptionally 抛给调用方: " + e.getCause()); } // obtrudeValue: 无论之前是否已完成,强制覆盖为新值(慎用) CompletableFuture forced = CompletableFuture.completedFuture("旧结果"); forced.obtrudeValue("强制更新的结果"); System.out.println("obtrudeValue 覆盖后: " + forced.join()); // cancel: 取消任务, isCancelled 与 isCompletedExceptionally 都会为 true CompletableFuture cancelling = new CompletableFuture<>(); System.out.println("cancel() 返回值: " + cancelling.cancel(true)); System.out.println("取消后 isCancelled: " + cancelling.isCancelled() + ", isCompletedExceptionally: " + cancelling.isCompletedExceptionally()); } ``` > 先说结论:`complete` 期待任务**还没有**完成;`obtrudeValue` 则不管状态**强制**覆盖,一般不要在生产乱用,否则正常逻辑都会被它"篡改"。 **使用场景:** - 用 `new CompletableFuture<>()` 当"闸门/栅栏",等某个外部事件(消息推送、第三方回调、人工审核)到来后再 `complete`,让等待方放行。 - 配合超时:等待超时后调 `completeExceptionally` 快速失败,别让调用方无限挂起。 - `cancel` 用于显式放弃一个已无意义的任务,例如用户离开了页面后取消剩余请求。 *** ## 九、线程池与 Async 变体 以上所有 `xxxAsync` 方法,我们没有特地指定线程池时,它们最终都跑在同一个地方——`ForkJoinPool.commonPool()`。这会带来两个大坑,必须提前讲清楚(详见 `Cf09_CustomExecutor`): ```java public static void main(String[] args) throws Exception { // 不指定线程池时, 默认全部任务都跑在 ForkJoinPool.commonPool 上 CompletableFuture defaultOne = CompletableFuture.supplyAsync(() -> { System.out.println("默认线程池: " + Thread.currentThread().getName()); return "OK"; }); defaultOne.join(); // 重活建议显式指定自定义线程池, 避免抢占公共池 ExecutorService pool = Executors.newFixedThreadPool(4); CompletableFuture 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 后缀的方法, 新任务在哪执行由"触发者"所在的线程决定, // 一般沿用前一个任务的执行线程; 带 Async 且不传池子时, 提交到 commonPool CompletableFuture 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 ``` 这里藏着 `CompletableFuture` 最容易踩的两个坑: > **坑一:commonPool 被阻塞任务饿死(Thread Starvation)** > `commonPool` 的线程数默认是 `CPU核心数 - 1`。当你在回调里再嵌套 `supplyAsync` 时,外层任务会占用 worker 等待,而内层任务又在排队等 worker 执行,**互等**,最终整个池子卡死。 > 生产环境里 RPC/HTTP/DB 等着**始终受限**,绝不要做不可控的阻塞操作。解决:给耗时的 I/O 调用显式传入一个**独立的 `ThreadPoolExecutor`**。 > **坑二:Async 后缀 ≠ 线程池更大,只是"换个池子提交"** > `thenApply`(无 Async)在哪个线程执行,取决于**谁触发了它**——往往沿用前一个任务的线程,甚至主线程(见上面输出第 4 行)。 > 所以别指望"加上 Async 就更快",它更多是在让你**指定提交的线程池**。 **使用场景与建议:** - 简单、短暂、CPU 密集的任务可以用默认池,省事。 - 遇到 I/O 阻塞、长耗时任务,务必 `new ThreadPoolExecutor(...)`(或固定大小池)并传给带 Async 的方法。 - 回调里切忌调用 `join()/get()` 等阻塞点完成自己,这是服务生产事故的高发区。 *** ## 十、总结与速查 至此,`CompletableFuture` 的核心骨架我们都过了一遍。把它当作一张"异步流程图"来记: | 需求 | 方法 | 回调形态 | 返回 | | --- | --- | --- | --- | | 无返回值异步任务 | `runAsync` | `Runnable` | `CompletableFuture` | | 有返回值异步任务 | `supplyAsync` | `Supplier` | `CompletableFuture` | | 拿来即用 | `completedFuture` | — | `CompletableFuture` | | 手拿上一个结果继续加工 | `thenApply` | `Function` | `CompletableFuture` | | 消费上一个结果 | `thenAccept` | `Consumer` | `CompletableFuture` | | 等上一环跑完即可 | `thenRun` | `Runnable` | `CompletableFuture` | | 两个都完成,合并结果 | `thenCombine` | `BiFunction` | `CompletableFuture` | | 两个都完成,消费结果 | `thenAcceptBoth` | `BiConsumer` | `CompletableFuture` | | 两个都完成,不等数据 | `runAfterBoth` | `Runnable` | `CompletableFuture` | | 谁先完成,加工谁的结果 | `applyToEither` | `Function` | `CompletableFuture` | | 谁先完成,消费谁的结果 | `acceptEither` | `Consumer` | `CompletableFuture` | | 谁先完成,只等时间点 | `runAfterEither` | `Runnable` | `CompletableFuture` | | 展平嵌套的异步调用 | `thenCompose` | `Function→CompletionStage` | `CompletableFuture` | | 全部完成(各自再 join 汇总) | `allOf` | — | `CompletableFuture` | | 任一完成(结果需转类型) | `anyOf` | — | `CompletableFuture` | | 失败时降级 | `exceptionally` | `Function` | `CompletableFuture` | | 成功失败都观察(不吞异常) | `whenComplete` | `BiConsumer` | 不可改结果 | | 成功失败都可处理(可恢复) | `handle` | `BiFunction` | `CompletableFuture` | | 手动设定结果 | `complete` | — | `boolean` | | 手动以异常结束 | `completeExceptionally` | — | `boolean` | | 强制覆盖结果(慎用) | `obtrudeValue` | — | `void` | | 取消任务 | `cancel` | — | `boolean` | 最后,把最容易犯的错再点一遍: 1. 默认线程池是 `ForkJoinPool.commonPool`,阻塞任务会把它饿死,I/O 要交给独立线程池。 2. `allOf` 返回的是 `Void`,汇总需自己逐个 `join()`。 3. `whenComplete` 不消化异常,异常会继续沿着链传下去。 4. 异常都会包装成 `CompletionException`,记得用 `getCause()` 取原始异常。 5. 回调里不要 `join()/get()` 阻塞自己的执行线程。 6. 非 Async 后缀的方法在"触发线程"上执行,别脑补它在公共池上跑。 *** > 版权声明:本文由 computrueFeture-demo 项目产出,适用于学习交流。