基于 :JDK 8+(
orTimeout、completeOnTimeout、Executor defaultExecutor()等标注 JDK 9+)解决什么问题:把"并发结果怎么拼、拼完怎么处理异常、哪条先完成用哪条"这类异步编排 逻辑,从手写回调嵌套 /
Future.get()阻塞轮询,升级为声明式、可组合、不阻塞的依赖链。本文覆盖 CompletableFuture 的定位、核心机制逐条剖析、编排主线流程与可运行示例、线程模型与陷阱、扩展点与 FAQ,目标是一篇"看完会编排、踩坑能定位"的深度笔记。
目录
- 定位与核心理论
- [1.1 要解决的痛点:Future 的局限](#1.1 要解决的痛点:Future 的局限)
- [1.2 双重身份:Future + CompletionStage](#1.2 双重身份:Future + CompletionStage)
- [1.3 异步编排方案定位表](#1.3 异步编排方案定位表)
- [设计思想与核心机制 ★★★](#设计思想与核心机制 ★★★)
- [2.1 设计思想:回调式依赖图](#2.1 设计思想:回调式依赖图)
- [2.2 启动任务:runAsync / supplyAsync](#2.2 启动任务:runAsync / supplyAsync)
- [2.3 串行依赖:thenApply / thenAccept / thenRun](#2.3 串行依赖:thenApply / thenAccept / thenRun)
- [2.4 扁平化依赖:thenCompose](#2.4 扁平化依赖:thenCompose)
- [2.5 并行组合:thenCombine / thenAcceptBoth / runAfterBoth](#2.5 并行组合:thenCombine / thenAcceptBoth / runAfterBoth)
- [2.6 竞速取优:applyToEither 家族](#2.6 竞速取优:applyToEither 家族)
- [2.7 异常处理:exceptionally / handle / whenComplete](#2.7 异常处理:exceptionally / handle / whenComplete)
- [2.8 主动完成与超时控制](#2.8 主动完成与超时控制)
- [2.9 多任务聚合:allOf / anyOf](#2.9 多任务聚合:allOf / anyOf)
- [2.10 执行线程模型:谁在跑回调?★★](#2.10 执行线程模型:谁在跑回调?★★)
- [2.11 内部实现机制浅析:无锁完成 + 等待者栈](#2.11 内部实现机制浅析:无锁完成 + 等待者栈)
- [主线流程剖析 ★★★](#主线流程剖析 ★★★)
- [3.1 典型场景:订单页聚合查询](#3.1 典型场景:订单页聚合查询)
- [3.2 编排全景图(mermaid)](#3.2 编排全景图(mermaid))
- [3.3 完整可运行示例](#3.3 完整可运行示例)
- [3.4 执行时序(mermaid)](#3.4 执行时序(mermaid))
- [3.5 类关系图(mermaid)](#3.5 类关系图(mermaid))
- 扩展点
- [4.1 线程池隔离与合理配置](#4.1 线程池隔离与合理配置)
- [4.2 超时兜底 orTimeout](#4.2 超时兜底 orTimeout)
- [4.3 与 Spring @Async 结合](#4.3 与 Spring @Async 结合)
- [4.4 JDK 21 虚拟线程下的取舍](#4.4 JDK 21 虚拟线程下的取舍)
- [4.5 调试与观测](#4.5 调试与观测)
- [常见问题 FAQ](#常见问题 FAQ)
1. 定位与核心理论
1.1 要解决的痛点:Future 的局限
Future 诞生于 JDK 1.5,只能表达"一个异步结果"。真实业务里往往是"多个服务并发查 → 拼装 → 判空 → 回退",而 Future 的表达力只到单任务:
| Future 痛点 | 具体表现 | 后果 |
|---|---|---|
| 阻塞获取 | get() 会阻塞调用线程直到完成 |
拿不到"回调式"结果,想并发就得 invokeAll 后逐个 get |
| 无串行管道 | 下一个计算依赖上一个结果时只能先 get 再提交新任务 |
每步都阻塞,异步链断成一截一截 |
| 无组合 | 没有"两个结果都完成才继续"的原语 | 手写 CountDownLatch / 轮询 |
| 无竞速 | 没有"谁先完成用谁"的原语 | 手写 CompletionService.take() 绕行 |
| 异常难处理 | 异常包在 ExecutionException,不消费就静默 |
要么吞异常要么到处 try |
关键认知:编排(Orchestration)要表达的不是"等一个结果",而是**"结果之间的依赖关系"**:串行(前→后)、并行(两边都完成)、竞速(先完成者胜)、容错(异常转默认值)。CompletableFuture 正是把这几类关系做成了方法。
1.2 双重身份:Future + CompletionStage
java
public class CompletableFuture<T> implements Future<T>, CompletionStage<T>
- 作为
Future:get() / get(long, TimeUnit) / isDone() / cancel(),可扔进线程池等老 API; - 作为
CompletionStage:thenXxx / combineXxx / eitherXxx / exceptionally......一系列返回新CompletionStage的方法------编排语法全在这里。
设计上有个值得记住的不对称:Future 面向"拿结果 "(阻塞/轮询),CompletionStage 面向"挂回调 "(事件驱动,线程永不阻塞在 get 上)。
1.3 异步编排方案定位表
| 方案 | 模型 | 表达力 | 阻塞? | 适合场景 |
|---|---|---|---|---|
Future / FutureTask |
单任务句柄 | 极弱 | 是 | 只需一个并发结果 |
| CompletableFuture | 回调依赖链 | 强:串行/并行/竞速/容错 | 否(消费时可选阻塞) | 请求内多服务编排、超时兜底、响应式网关 |
| RxJava / Reactor(Mono/Flux) | 响应式流 + 背压 | 最强:重试/熔断/流式/背压 | 否 | 全链路响应式、大数据流、长连接 |
| 虚拟线程 + 同步代码 | 阻塞式但廉价的线程 | 结构简单 | 是(虚拟线程上便宜) | 普通 IO 密集服务"并发改同步"(见 [4.4](#方案 模型 表达力 阻塞? 适合场景 Future / FutureTask 单任务句柄 极弱 是 只需一个并发结果 CompletableFuture 回调依赖链 强:串行/并行/竞速/容错 否(消费时可选阻塞) 请求内多服务编排、超时兜底、响应式网关 RxJava / Reactor(Mono/Flux) 响应式流 + 背压 最强:重试/熔断/流式/背压 否 全链路响应式、大数据流、长连接 虚拟线程 + 同步代码 阻塞式但廉价的线程 结构简单 是(虚拟线程上便宜) 普通 IO 密集服务"并发改同步"(见 4.4))) |
⭐ 一句话概括 :如果只是"并发调 3 个服务收结果",虚拟线程同步代码最省心;如果要在一个请求内把异步结果串成管道、做竞速与超时、喂给异步框架,CompletableFuture 是标准答案。
2. 设计思想与核心机制 ★★★
2.1 设计思想:回调式依赖图
#mermaid-svg-1UMxdH0nvAsaO7Th{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1UMxdH0nvAsaO7Th .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1UMxdH0nvAsaO7Th .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1UMxdH0nvAsaO7Th .error-icon{fill:#552222;}#mermaid-svg-1UMxdH0nvAsaO7Th .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1UMxdH0nvAsaO7Th .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1UMxdH0nvAsaO7Th .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1UMxdH0nvAsaO7Th .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1UMxdH0nvAsaO7Th .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1UMxdH0nvAsaO7Th .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1UMxdH0nvAsaO7Th .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1UMxdH0nvAsaO7Th .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1UMxdH0nvAsaO7Th .marker.cross{stroke:#333333;}#mermaid-svg-1UMxdH0nvAsaO7Th svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1UMxdH0nvAsaO7Th p{margin:0;}#mermaid-svg-1UMxdH0nvAsaO7Th .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1UMxdH0nvAsaO7Th .cluster-label text{fill:#333;}#mermaid-svg-1UMxdH0nvAsaO7Th .cluster-label span{color:#333;}#mermaid-svg-1UMxdH0nvAsaO7Th .cluster-label span p{background-color:transparent;}#mermaid-svg-1UMxdH0nvAsaO7Th .label text,#mermaid-svg-1UMxdH0nvAsaO7Th span{fill:#333;color:#333;}#mermaid-svg-1UMxdH0nvAsaO7Th .node rect,#mermaid-svg-1UMxdH0nvAsaO7Th .node circle,#mermaid-svg-1UMxdH0nvAsaO7Th .node ellipse,#mermaid-svg-1UMxdH0nvAsaO7Th .node polygon,#mermaid-svg-1UMxdH0nvAsaO7Th .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1UMxdH0nvAsaO7Th .rough-node .label text,#mermaid-svg-1UMxdH0nvAsaO7Th .node .label text,#mermaid-svg-1UMxdH0nvAsaO7Th .image-shape .label,#mermaid-svg-1UMxdH0nvAsaO7Th .icon-shape .label{text-anchor:middle;}#mermaid-svg-1UMxdH0nvAsaO7Th .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1UMxdH0nvAsaO7Th .rough-node .label,#mermaid-svg-1UMxdH0nvAsaO7Th .node .label,#mermaid-svg-1UMxdH0nvAsaO7Th .image-shape .label,#mermaid-svg-1UMxdH0nvAsaO7Th .icon-shape .label{text-align:center;}#mermaid-svg-1UMxdH0nvAsaO7Th .node.clickable{cursor:pointer;}#mermaid-svg-1UMxdH0nvAsaO7Th .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1UMxdH0nvAsaO7Th .arrowheadPath{fill:#333333;}#mermaid-svg-1UMxdH0nvAsaO7Th .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1UMxdH0nvAsaO7Th .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1UMxdH0nvAsaO7Th .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1UMxdH0nvAsaO7Th .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1UMxdH0nvAsaO7Th .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1UMxdH0nvAsaO7Th .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1UMxdH0nvAsaO7Th .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1UMxdH0nvAsaO7Th .cluster text{fill:#333;}#mermaid-svg-1UMxdH0nvAsaO7Th .cluster span{color:#333;}#mermaid-svg-1UMxdH0nvAsaO7Th div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1UMxdH0nvAsaO7Th .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1UMxdH0nvAsaO7Th rect.text{fill:none;stroke-width:0;}#mermaid-svg-1UMxdH0nvAsaO7Th .icon-shape,#mermaid-svg-1UMxdH0nvAsaO7Th .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1UMxdH0nvAsaO7Th .icon-shape p,#mermaid-svg-1UMxdH0nvAsaO7Th .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1UMxdH0nvAsaO7Th .icon-shape .label rect,#mermaid-svg-1UMxdH0nvAsaO7Th .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1UMxdH0nvAsaO7Th .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1UMxdH0nvAsaO7Th .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1UMxdH0nvAsaO7Th :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} supplyAsync 任务 A
price(价格)
thenApply 加工A
- 税
supplyAsync 任务 B
stock(库存)
thenCombine 两者齐备
exceptionally
失败给默认值
- 每个方法返回一个新的 CompletableFuture(而不是改自己),依赖关系在创建时就"接"上;
- 整个编排在创建阶段零阻塞,线程只在任务真正执行时被占用;
- 上游完成时自动触发下游回调,像多米诺骨牌,一条链只需一个初始任务线程起爆;
- 结果只向下游传播:下游拿不到上游的中间步骤引用(除非显式保存),天然隔离。
2.2 启动任务:runAsync / supplyAsync
| 方法 | 函数签名(去泛型) | 返回值 | 典型用途 |
|---|---|---|---|
runAsync(Runnable) |
无入参无返回值 | CompletableFuture<Void> |
发通知、写日志、预热缓存 |
supplyAsync(Supplier) |
无入参有返回值 | CompletableFuture<T> |
查库、调远程、算结果 |
runAsync(r, executor) / supplyAsync(s, executor) |
同上 + 执行器 | 同左 | 必须传入自定义线程池 (见 [4.1](#方法 函数签名(去泛型) 返回值 典型用途 runAsync(Runnable) 无入参无返回值 CompletableFuture |
java
// 片段:三种启动方式
CompletableFuture<Void> f1 = CompletableFuture.runAsync(() -> System.out.println("发短信"));
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> "订单号 10086"); // 默认 ForkJoinPool.commonPool
CompletableFuture<String> f3 = CompletableFuture.supplyAsync(() -> httpGet("/price"), pool); // 自定义池
2.3 串行依赖:thenApply / thenAccept / thenRun
上游完成后在其"执行链"上继续(线程归属见 2.10):
| 方法 | 入参函数 | 产出 | 时机 |
|---|---|---|---|
thenApply(fn) |
T -> U |
新值 U |
需要把上一步结果加工成新结果 |
thenAccept(consumer) |
T -> void |
无(Void) |
结果不再传递,只"消费"(写库、刷缓存) |
thenRun(action) |
() -> void |
无 | 连上一步结果都不关心(埋点、计时) |
java
// 片段:串行管道 ------ 查价 → 加税 → 落缓存,全程无阻塞
CompletableFuture.supplyAsync(() -> httpGet("/price"), pool) // 远程查价
.thenApply(Double::parseDouble) // 字符串转数(加工)
.thenApply(p -> p * 1.13) // 加 13% 税
.thenAccept(finalPrice -> cache.put(key, finalPrice)) // 终点消费,不再传递
.thenRun(() -> System.out.println("缓存写入完成")); // 连结果都不看
⚠️ thenAccept/thenRun 之后链就断了(返回 Void),想继续传值务必用 thenApply。
2.4 扁平化依赖:thenCompose
当"下一步本身又是一个异步任务(要再提交线程/再发请求)"时,必须用 thenCompose:
java
// ❌ 反例:thenApply 里包 CompletableFuture ------ 得到"Future 套 Future"
CompletableFuture<CompletableFuture<Order>> nested =
CompletableFuture.supplyAsync(() -> findUser("u1"), pool)
.thenApply(user -> CompletableFuture.supplyAsync(() -> findOrder(user), pool));
// ✅ thenCompose:展平 ------ 链上始终是 CompletableFuture<Order>
CompletableFuture<Order> flat =
CompletableFuture.supplyAsync(() -> findUser("u1"), pool)
.thenCompose(user -> CompletableFuture.supplyAsync(() -> findOrder(user), pool));
关键认知 :
thenApply的映射函数同步返回普通值 ;thenCompose的映射函数返回一个 CompletionStage (典型的"异步调异步")。判断口诀:回调体里还有supplyAsync/thenApply吗?有 → 用thenCompose收扁。扁平的链异常传播、超时、调试都更干净。
2.5 并行组合:thenCombine / thenAcceptBoth / runAfterBoth
两个 CompletableFuture 都完成才算完成(fan-in 汇聚,JDK8 无"多个同时等"的原语,交给 2.9 的 allOf):
| 方法 | 函数 | 何时触发 |
|---|---|---|
thenCombine(other, (a,b) -> ...) |
(T,U) -> R,两值合成新值 |
两边都正常完成 |
thenAcceptBoth(other, (a,b) -> ...) |
(T,U) -> void |
两边都完成(只消费) |
runAfterBoth(other, action) |
() -> void |
两边都完成(值不重要) |
带 Async 变体 |
同上 | 回调丢进执行器执行 |
java
// 片段:双路并行 → 合成
CompletableFuture<Integer> price = CompletableFuture.supplyAsync(() -> queryPrice(), pool);
CompletableFuture<Integer> stock = CompletableFuture.supplyAsync(() -> queryStock(), pool);
CompletableFuture<String> result = price.thenCombine(stock, (p, s) ->
"价格=" + p + ", 库存=" + s + (s > 0 ? " 可下单" : " 缺货"));
System.out.println(result.join()); // 演示用;生产建议自定义池 + 超时兜底
2.6 竞速取优:applyToEither 家族
两个 CompletableFuture 谁先完成(含异常完成)就用谁,适用于主备数据源、超时旁路、多通道取最快:
| 方法 | 函数 | 语义 |
|---|---|---|
applyToEither(other, fn) |
胜者的值 -> 新值 | 两源竞速,胜者加工 |
acceptEither(other, consumer) |
胜者值 -> void | 只消费胜者 |
runAfterEither(other, action) |
() -> void | 无所谓值,先到先触发 |
java
// 片段:主备数据源竞速 ------ 任一返回即用,不等慢的
CompletableFuture<Price> primary = CompletableFuture.supplyAsync(() -> rpcA("查价格"), pool);
CompletableFuture<Price> secondary = CompletableFuture.supplyAsync(() -> rpcB("查价格"), pool);
primary.applyToEither(secondary, Price::round) // 谁先完成取谁,加工后返回
.thenAccept(p -> System.out.println("胜出价格=" + p));
⚠️ 竞速结果是"先到先得",晚到的任务并不会被取消,它仍在后台跑完(但它的结果已无人消费)。
2.7 异常处理:exceptionally / handle / whenComplete
CF 的异常默认"潜伏":链上没有消费点的话谁也不知道出错了(见 FAQ Q6)。三个兜底方法职责分明:
| 方法 | 签名 | 触发条件 | 能恢复吗 | 典型用途 |
|---|---|---|---|---|
exceptionally(fn) |
Throwable -> T |
仅异常时 | ✅ 返回替代值,链恢复 | 失败降级:给默认值、走缓存 |
handle((v,e) -> R) |
值或异常 -> R | 总是触发 | ✅ 自己决定返回什么 | 终结点统一转 DTO/响应码 |
whenComplete((v,e) -> a) |
值+异常 -> void | 总是触发 | ❌ 值/异常原样透传 | 日志、埋点、释放资源 |
java
// 片段:同一条异常链,三种接法对比
CompletableFuture<String> fault =
CompletableFuture.supplyAsync(() -> { if (Math.random() < 0.5) throw new RuntimeException("上游挂了"); return "ok"; }, pool);
fault.exceptionally(e -> { System.out.println("降级: " + e.getCause()); return "fallback"; }) // ① 异常才接,恢复为 fallback
.handle((v, e) -> v != null ? "SUCCESS:" + v : "FAIL:" + e.getCause()) // ② 无论成败都给一个最终值
.whenComplete((v, e) -> System.out.println("终态监控: v=" + v + ", e=" + e)) // ③ 只看一眼,值继续往下传
.join(); // ② 已吞掉异常,这里不会再抛
关键认知:
exceptionally/handle接住的是CompletionException(包装异常) ,业务原因在e.getCause();whenComplete不改变结果 ------异常仍会传给下一个下游,链上没人接就继续潜伏,最后一环必须join()/get()或再挂消费点把它"消化";- 三种方法都只拦截"链上出现的异常",本身没抛新异常且正常返回,链就恢复为成功。
2.8 主动完成与超时控制
CF 除了"跑完自动完成",还允许外部线程"替它完成"------这就是把回调式转为命令式的后门,也是实现超时/取消的基石:
| 方法 | 语义 |
|---|---|
complete(v) |
未完成时置为成功;返回是否本轮由调用者完成(CAS 结果) |
completeExceptionally(t) |
未完成时置为异常完成 |
cancel(mayInterrupt) |
置为"已取消",⚠️ 不中断正在跑的任务线程(见 FAQ Q10) |
get(long, TimeUnit) |
阻塞式限时等待(JDK8 起就有),超时抛 TimeoutException |
orTimeout(long, TimeUnit) ⭐JDK9+ |
非阻塞 超时:到点自动 completeExceptionally(new TimeoutException()),后续链收到异常 |
completeOnTimeout(v, long, TimeUnit) ⭐JDK9+ |
非阻塞超时降级:到点自动 complete(v) 默认值,链继续成功走 |
delayedExecutor(...) ⭐JDK9+ |
调度型执行器,可与 orTimeout 同族使用 |
java
// 片段:JDK9+ 非阻塞超时 ------ 主链路到点放行,绝不傻等
CompletableFuture<Price> slow = CompletableFuture.supplyAsync(() -> rpcSlow(), pool);
Price ok = slow.orTimeout(3, TimeUnit.SECONDS) // 3s 没完成 → 这条链以 TimeoutException 结束
.exceptionally(e -> Price.DEFAULT); // 接住超时,降级默认价
⚠️ 超时只"放行调用链" :slow 那个任务若还在跑,不会因超时被中断(线程照常执行完,结果被丢弃)------要真正停掉必须靠任务内部协作检查取消标记。
2.9 多任务聚合:allOf / anyOf
两个以上任务的汇合原语(JDK8 提供,替代手写 CountDownLatch):
| 方法 | 语义 | 典型坑 |
|---|---|---|
allOf(cf...) |
返回 CompletableFuture<Void>,全部完成才完成;任一异常完成则整链立即异常 |
它不收集各任务返回值 ,结果要自己留引用后 join() 逐个取 |
anyOf(cf...) |
返回 CompletableFuture<Object>,任意一个完成即完成 |
返回值是 Object 需强转;竞速场景用 [2.6](#方法 语义 典型坑 allOf(cf...) 返回 CompletableFuture |
java
// 片段:allOf 等待 3 路并行,完成后统一收集(常用写法)
List<CompletableFuture<String>> futures = List.of(
CompletableFuture.supplyAsync(() -> query("价格"), pool),
CompletableFuture.supplyAsync(() -> query("库存"), pool),
CompletableFuture.supplyAsync(() -> query("优惠"), pool));
CompletableFuture<List<String>> all = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.thenApply(v -> futures.stream().map(CompletableFuture::join).collect(Collectors.toList()));
// ↑ 注意:v 是 Void,各任务结果在闭包里 join 取出 ------ 此时必已全部完成,join 不会阻塞
2.10 执行线程模型:谁在跑回调?★★
90% 的线上事故源于对"回调在哪个线程执行"的误解。规则一句话:回调在"触发它的线程"上原地执行(无 Async 后缀时);带 Async 后缀则提交到指定执行器。
| 写法 | 执行线程 | 备注 |
|---|---|---|
无后缀(thenApply 等) |
上游完成的那个线程 ;若注册时上游早已完成,则在注册线程立即执行 | 意味着:如果你在"Netty IO 线程 / 请求线程"上调用 complete(),整条无后缀链都在该线程原地跑------阻塞它等于阻塞服务 |
xxxAsync(fn) |
ForkJoinPool.commonPool |
默认执行器是守护线程 + 全局共享(详见 [4.1](#写法 执行线程 备注 无后缀(thenApply 等) 上游完成的那个线程;若注册时上游早已完成,则在注册线程立即执行 意味着:如果你在"Netty IO 线程 / 请求线程"上调用 complete(),整条无后缀链都在该线程原地跑——阻塞它等于阻塞服务 xxxAsync(fn) ForkJoinPool.commonPool 默认执行器是守护线程 + 全局共享(详见 4.1) xxxAsync(fn, executor) 指定的 executor 生产首选:线程归属清晰、池可监控)) |
xxxAsync(fn, executor) |
指定的 executor |
生产首选:线程归属清晰、池可监控 |
java
// 片段:线程归属验证(配合线程名观察)
CompletableFuture.supplyAsync(() -> { System.out.println("任务线程: " + Thread.currentThread().getName()); return 1; }, pool)
.thenApply(x -> { System.out.println("无后缀回调线程: " + Thread.currentThread().getName()); return x + 1; }) // 与任务线程相同
.thenApplyAsync(x -> { System.out.println("Async回调线程: " + Thread.currentThread().getName()); return x + 1; }) // 提交回 pool(或 commonPool)
.join();
关键认知 :
thenApply(无 Async)不等于"新开线程",它只是"注册一个回调"。真正的线程调度只发生在supplyAsync/runAsync和xxxAsync处。想让编排里每一步都落到你的线程池,每步都得带Async+ 同一个 executor ------这也是很多团队约定"链内统一thenApplyAsync(x -> ..., pool)"的原因。
2.11 内部实现机制浅析:无锁完成 + 等待者栈
理解内部机制不是为了重写,而是为了解释三类现象:为什么异常会潜伏、为什么无后缀回调不新起线程、为什么并发安全。
#mermaid-svg-FDpkfOyNWtPwN6uy{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-FDpkfOyNWtPwN6uy .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-FDpkfOyNWtPwN6uy .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-FDpkfOyNWtPwN6uy .error-icon{fill:#552222;}#mermaid-svg-FDpkfOyNWtPwN6uy .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-FDpkfOyNWtPwN6uy .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-FDpkfOyNWtPwN6uy .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-FDpkfOyNWtPwN6uy .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-FDpkfOyNWtPwN6uy .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-FDpkfOyNWtPwN6uy .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-FDpkfOyNWtPwN6uy .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-FDpkfOyNWtPwN6uy .marker{fill:#333333;stroke:#333333;}#mermaid-svg-FDpkfOyNWtPwN6uy .marker.cross{stroke:#333333;}#mermaid-svg-FDpkfOyNWtPwN6uy svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-FDpkfOyNWtPwN6uy p{margin:0;}#mermaid-svg-FDpkfOyNWtPwN6uy defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-FDpkfOyNWtPwN6uy g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-FDpkfOyNWtPwN6uy g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-FDpkfOyNWtPwN6uy g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-FDpkfOyNWtPwN6uy g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-FDpkfOyNWtPwN6uy g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-FDpkfOyNWtPwN6uy .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-FDpkfOyNWtPwN6uy .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-FDpkfOyNWtPwN6uy .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-FDpkfOyNWtPwN6uy .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-FDpkfOyNWtPwN6uy .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-FDpkfOyNWtPwN6uy .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-FDpkfOyNWtPwN6uy .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-FDpkfOyNWtPwN6uy .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-FDpkfOyNWtPwN6uy .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-FDpkfOyNWtPwN6uy .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-FDpkfOyNWtPwN6uy .edgeLabel .label text{fill:#333;}#mermaid-svg-FDpkfOyNWtPwN6uy .label div .edgeLabel{color:#333;}#mermaid-svg-FDpkfOyNWtPwN6uy .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-FDpkfOyNWtPwN6uy .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-FDpkfOyNWtPwN6uy .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-FDpkfOyNWtPwN6uy .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-FDpkfOyNWtPwN6uy .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-FDpkfOyNWtPwN6uy .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FDpkfOyNWtPwN6uy .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FDpkfOyNWtPwN6uy #statediagram-barbEnd{fill:#333333;}#mermaid-svg-FDpkfOyNWtPwN6uy .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-FDpkfOyNWtPwN6uy .cluster-label,#mermaid-svg-FDpkfOyNWtPwN6uy .nodeLabel{color:#131300;}#mermaid-svg-FDpkfOyNWtPwN6uy .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-FDpkfOyNWtPwN6uy .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-FDpkfOyNWtPwN6uy .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-FDpkfOyNWtPwN6uy .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-FDpkfOyNWtPwN6uy .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-FDpkfOyNWtPwN6uy .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-FDpkfOyNWtPwN6uy .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-FDpkfOyNWtPwN6uy .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-FDpkfOyNWtPwN6uy .note-edge{stroke-dasharray:5;}#mermaid-svg-FDpkfOyNWtPwN6uy .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-FDpkfOyNWtPwN6uy .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-FDpkfOyNWtPwN6uy .statediagram-note text{fill:black;}#mermaid-svg-FDpkfOyNWtPwN6uy .statediagram-note .nodeLabel{color:black;}#mermaid-svg-FDpkfOyNWtPwN6uy .statediagram .edgeLabel{color:red;}#mermaid-svg-FDpkfOyNWtPwN6uy #dependencyStart,#mermaid-svg-FDpkfOyNWtPwN6uy #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-FDpkfOyNWtPwN6uy .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-FDpkfOyNWtPwN6uy :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 创建(无锁状态)
线程A CAS 置结果值
CAS 置 AltResult 异常对象
cancel() 置取消标记
完成者沿等待者栈同步触发下游
Pending
Normal
Exceptional
Cancelled
Trigger
- 状态字段 :单个无锁(volatile + CAS)状态位编码"未完成 / 正常结果 / 异常结果(
AltResult包装)/ 已取消",与 \[ConcurrentHashMap源码解析] 的扩容 CAS 是同一类"乐观无锁写"思路------谁 CAS 成功谁是唯一完成者,天然线程安全; - 等待者栈 :链上每个下游(
thenApply注册的回调)在创建时被压进上游的"等待者栈";上游完成时,完成者 CAS 成功后负责弹出整个栈、沿依赖逐个触发; - 触发即执行 :无后缀回调由完成者调用线程直接跑 (这正是 2.10 线程归属的由来);
Async变体则把"触发动作"封装成任务提交给执行器; - 异常传播 :值/异常统一存在状态位里往下游传,下游不消费(不再挂回调或 join)异常就停在状态里------潜伏的根源。
⭐ 这解释了 FAQ 里大半问题的"为什么"。
3. 主线流程剖析 ★★★
3.1 典型场景:订单页聚合查询
电商订单页通常要同时拿"价格(远程服务 A)+ 库存(远程服务 B)+ 优惠券(远程服务 C)",串行要 3×RTT,并行编排只需 1×RTT + 计算开销。编排主线:分叉(3 个 supplyAsync)→ 汇合(thenCombine/allOf)→ 兜底(exceptionally)→ 超时(orTimeout)→ 消费(join)。
3.2 编排全景图(mermaid)
#mermaid-svg-vzuA84SkuS7GAAXf{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vzuA84SkuS7GAAXf .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vzuA84SkuS7GAAXf .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vzuA84SkuS7GAAXf .error-icon{fill:#552222;}#mermaid-svg-vzuA84SkuS7GAAXf .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vzuA84SkuS7GAAXf .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vzuA84SkuS7GAAXf .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vzuA84SkuS7GAAXf .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vzuA84SkuS7GAAXf .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vzuA84SkuS7GAAXf .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vzuA84SkuS7GAAXf .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vzuA84SkuS7GAAXf .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vzuA84SkuS7GAAXf .marker.cross{stroke:#333333;}#mermaid-svg-vzuA84SkuS7GAAXf svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vzuA84SkuS7GAAXf p{margin:0;}#mermaid-svg-vzuA84SkuS7GAAXf .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-vzuA84SkuS7GAAXf .cluster-label text{fill:#333;}#mermaid-svg-vzuA84SkuS7GAAXf .cluster-label span{color:#333;}#mermaid-svg-vzuA84SkuS7GAAXf .cluster-label span p{background-color:transparent;}#mermaid-svg-vzuA84SkuS7GAAXf .label text,#mermaid-svg-vzuA84SkuS7GAAXf span{fill:#333;color:#333;}#mermaid-svg-vzuA84SkuS7GAAXf .node rect,#mermaid-svg-vzuA84SkuS7GAAXf .node circle,#mermaid-svg-vzuA84SkuS7GAAXf .node ellipse,#mermaid-svg-vzuA84SkuS7GAAXf .node polygon,#mermaid-svg-vzuA84SkuS7GAAXf .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vzuA84SkuS7GAAXf .rough-node .label text,#mermaid-svg-vzuA84SkuS7GAAXf .node .label text,#mermaid-svg-vzuA84SkuS7GAAXf .image-shape .label,#mermaid-svg-vzuA84SkuS7GAAXf .icon-shape .label{text-anchor:middle;}#mermaid-svg-vzuA84SkuS7GAAXf .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vzuA84SkuS7GAAXf .rough-node .label,#mermaid-svg-vzuA84SkuS7GAAXf .node .label,#mermaid-svg-vzuA84SkuS7GAAXf .image-shape .label,#mermaid-svg-vzuA84SkuS7GAAXf .icon-shape .label{text-align:center;}#mermaid-svg-vzuA84SkuS7GAAXf .node.clickable{cursor:pointer;}#mermaid-svg-vzuA84SkuS7GAAXf .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vzuA84SkuS7GAAXf .arrowheadPath{fill:#333333;}#mermaid-svg-vzuA84SkuS7GAAXf .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vzuA84SkuS7GAAXf .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vzuA84SkuS7GAAXf .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vzuA84SkuS7GAAXf .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vzuA84SkuS7GAAXf .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vzuA84SkuS7GAAXf .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vzuA84SkuS7GAAXf .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vzuA84SkuS7GAAXf .cluster text{fill:#333;}#mermaid-svg-vzuA84SkuS7GAAXf .cluster span{color:#333;}#mermaid-svg-vzuA84SkuS7GAAXf div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-vzuA84SkuS7GAAXf .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vzuA84SkuS7GAAXf rect.text{fill:none;stroke-width:0;}#mermaid-svg-vzuA84SkuS7GAAXf .icon-shape,#mermaid-svg-vzuA84SkuS7GAAXf .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vzuA84SkuS7GAAXf .icon-shape p,#mermaid-svg-vzuA84SkuS7GAAXf .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vzuA84SkuS7GAAXf .icon-shape .label rect,#mermaid-svg-vzuA84SkuS7GAAXf .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vzuA84SkuS7GAAXf .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vzuA84SkuS7GAAXf .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vzuA84SkuS7GAAXf :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 异常
正常
主线程发起订单页聚合
F1 supplyAsync 查价格 rpcA
F2 supplyAsync 查库存 rpcB
F3 supplyAsync 查优惠券 rpcC
F1 thenCombine F2 得 价格+库存
再 thenCombine F3 三路全量汇聚
exceptionally 任一失败
降级: 默认价/缓存价 恢复链路
汇总结果
orTimeout 3s 兜底
主线程 join 消费终值
渲染订单页 / 写响应
3.3 完整可运行示例
java
// OrderPageDemo.java ------ JDK 9+ 可直接运行;演示: 并行分叉 → 逐级汇合 → 异常降级 → 超时兜底 → 消费
import java.time.LocalTime;
import java.util.concurrent.*;
import java.util.concurrent.CompletableFuture;
public class OrderPageDemo {
static final ExecutorService IO_POOL = new ThreadPoolExecutor(
8, 16, 30, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(500),
new ThreadFactory() { // 命名的工厂,日志里好认
private final java.util.concurrent.atomic.AtomicInteger n = new java.util.concurrent.atomic.AtomicInteger();
public Thread newThread(Runnable r) { return new Thread(r, "order-io-" + n.incrementAndGet()); }
},
new ThreadPoolExecutor.AbortPolicy());
// 模拟远程调用:name 是服务名,delay 是耗时
static int rpc(String name, long delay) {
System.out.println(LocalTime.now() + " [" + Thread.currentThread().getName() + "] 调用 " + name + " 开始");
try { Thread.sleep(delay); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
System.out.println(LocalTime.now() + " [" + Thread.currentThread().getName() + "] 调用 " + name + " 返回");
return delay >= 0 ? (int) delay : 0;
}
public static void main(String[] args) {
ExecutorService pool = IO_POOL; // 生产记得优雅关闭
long t0 = System.currentTimeMillis();
// ① 分叉:三路并行远程调用
CompletableFuture<Integer> priceF = CompletableFuture.supplyAsync(() -> rpc("价格服务", 500), pool);
CompletableFuture<Integer> stockF = CompletableFuture.supplyAsync(() -> rpc("库存服务", 300), pool);
CompletableFuture<Integer> coupF = CompletableFuture.supplyAsync(() -> rpc("优惠服务", 200), pool);
// ② 汇合:two-step 汇聚
CompletableFuture<String> page = priceF.thenCombine(stockF, (price, stock) -> price + "元/库存" + stock)
.thenCombine(coupF, (info, coup) -> info + "/优惠" + coup + "元");
// ③ 异常降级:任一上游异常(含被②传播)→ 给可展示的兜底文本,链恢复成功
CompletableFuture<String> guarded = page.exceptionally(e ->
"价格服务暂不可用, 展示缓存价 0 元(原因: " + e.getCause().getMessage() + ")");
// ④ 超时兜底: 整链最多等 2s,超时置为异常再由 exceptionally 处理(注意顺序)
String result = guarded.orTimeout(2, TimeUnit.SECONDS)
.exceptionally(e -> "聚合超时, 显示兜底页; 原因=" + e.getCause())
.join(); // ⑤ 主线程消费 ------ 唯一一次真正阻塞
System.out.println("聚合结果: " + result);
System.out.println("总耗时: " + (System.currentTimeMillis() - t0) + "ms (并行 500ms 而非串行 1000ms)");
pool.shutdown(); // 释放线程池,进程才能退出
}
}
✅ 示例自检:三个模拟 RPC 各 200~500ms,并行编排总耗时 ≈ 500ms+(最大者) ,远小于串行的 1000ms+;
orTimeout用的是 JDK9+ API(JDK8 请改为get(2, TimeUnit.SECONDS)+ 手工兜底,或升级)。
3.4 执行时序(mermaid)
业务线程池 order-io-* 主线程 main 业务线程池 order-io-* 主线程 main #mermaid-svg-dN19gZcpMFvnnM6j{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dN19gZcpMFvnnM6j .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dN19gZcpMFvnnM6j .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dN19gZcpMFvnnM6j .error-icon{fill:#552222;}#mermaid-svg-dN19gZcpMFvnnM6j .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dN19gZcpMFvnnM6j .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dN19gZcpMFvnnM6j .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dN19gZcpMFvnnM6j .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dN19gZcpMFvnnM6j .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dN19gZcpMFvnnM6j .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dN19gZcpMFvnnM6j .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dN19gZcpMFvnnM6j .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dN19gZcpMFvnnM6j .marker.cross{stroke:#333333;}#mermaid-svg-dN19gZcpMFvnnM6j svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dN19gZcpMFvnnM6j p{margin:0;}#mermaid-svg-dN19gZcpMFvnnM6j .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-dN19gZcpMFvnnM6j text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-dN19gZcpMFvnnM6j .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-dN19gZcpMFvnnM6j .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-dN19gZcpMFvnnM6j .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-dN19gZcpMFvnnM6j .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-dN19gZcpMFvnnM6j #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-dN19gZcpMFvnnM6j .sequenceNumber{fill:white;}#mermaid-svg-dN19gZcpMFvnnM6j #sequencenumber{fill:#333;}#mermaid-svg-dN19gZcpMFvnnM6j #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-dN19gZcpMFvnnM6j .messageText{fill:#333;stroke:none;}#mermaid-svg-dN19gZcpMFvnnM6j .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-dN19gZcpMFvnnM6j .labelText,#mermaid-svg-dN19gZcpMFvnnM6j .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-dN19gZcpMFvnnM6j .loopText,#mermaid-svg-dN19gZcpMFvnnM6j .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-dN19gZcpMFvnnM6j .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-dN19gZcpMFvnnM6j .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-dN19gZcpMFvnnM6j .noteText,#mermaid-svg-dN19gZcpMFvnnM6j .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-dN19gZcpMFvnnM6j .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-dN19gZcpMFvnnM6j .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-dN19gZcpMFvnnM6j .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-dN19gZcpMFvnnM6j .actorPopupMenu{position:absolute;}#mermaid-svg-dN19gZcpMFvnnM6j .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-dN19gZcpMFvnnM6j .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-dN19gZcpMFvnnM6j .actor-man circle,#mermaid-svg-dN19gZcpMFvnnM6j line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-dN19gZcpMFvnnM6j :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 全程主线程只在最后 join 时短暂阻塞 编排过程主线程不空等 supplyAsync 提交 任务A(价) / B(库) / C(券) 1 三任务并行执行 2 A 完成后 触发 thenCombine 回调(仍在线程A上) 3 B 完成 → 回调合流 4 C 完成 → 二次合流 5 join 返回终值(仅此一处阻塞) 6
3.5 类关系图(mermaid)
#mermaid-svg-ZjWyhBxrBaChVRYO{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ZjWyhBxrBaChVRYO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ZjWyhBxrBaChVRYO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ZjWyhBxrBaChVRYO .error-icon{fill:#552222;}#mermaid-svg-ZjWyhBxrBaChVRYO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ZjWyhBxrBaChVRYO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ZjWyhBxrBaChVRYO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ZjWyhBxrBaChVRYO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ZjWyhBxrBaChVRYO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ZjWyhBxrBaChVRYO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ZjWyhBxrBaChVRYO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ZjWyhBxrBaChVRYO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ZjWyhBxrBaChVRYO .marker.cross{stroke:#333333;}#mermaid-svg-ZjWyhBxrBaChVRYO svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ZjWyhBxrBaChVRYO p{margin:0;}#mermaid-svg-ZjWyhBxrBaChVRYO g.classGroup text{fill:#9370DB;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#mermaid-svg-ZjWyhBxrBaChVRYO g.classGroup text .title{font-weight:bolder;}#mermaid-svg-ZjWyhBxrBaChVRYO .cluster-label text{fill:#333;}#mermaid-svg-ZjWyhBxrBaChVRYO .cluster-label span{color:#333;}#mermaid-svg-ZjWyhBxrBaChVRYO .cluster-label span p{background-color:transparent;}#mermaid-svg-ZjWyhBxrBaChVRYO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ZjWyhBxrBaChVRYO .cluster text{fill:#333;}#mermaid-svg-ZjWyhBxrBaChVRYO .cluster span{color:#333;}#mermaid-svg-ZjWyhBxrBaChVRYO .nodeLabel,#mermaid-svg-ZjWyhBxrBaChVRYO .edgeLabel{color:#131300;}#mermaid-svg-ZjWyhBxrBaChVRYO .edgeLabel .label rect{fill:#ECECFF;}#mermaid-svg-ZjWyhBxrBaChVRYO .label text{fill:#131300;}#mermaid-svg-ZjWyhBxrBaChVRYO .labelBkg{background:#ECECFF;}#mermaid-svg-ZjWyhBxrBaChVRYO .edgeLabel .label span{background:#ECECFF;}#mermaid-svg-ZjWyhBxrBaChVRYO .classTitle{font-weight:bolder;}#mermaid-svg-ZjWyhBxrBaChVRYO .node rect,#mermaid-svg-ZjWyhBxrBaChVRYO .node circle,#mermaid-svg-ZjWyhBxrBaChVRYO .node ellipse,#mermaid-svg-ZjWyhBxrBaChVRYO .node polygon,#mermaid-svg-ZjWyhBxrBaChVRYO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ZjWyhBxrBaChVRYO .divider{stroke:#9370DB;stroke-width:1;}#mermaid-svg-ZjWyhBxrBaChVRYO g.clickable{cursor:pointer;}#mermaid-svg-ZjWyhBxrBaChVRYO g.classGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-ZjWyhBxrBaChVRYO g.classGroup line{stroke:#9370DB;stroke-width:1;}#mermaid-svg-ZjWyhBxrBaChVRYO .classLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-ZjWyhBxrBaChVRYO .classLabel .label{fill:#9370DB;font-size:10px;}#mermaid-svg-ZjWyhBxrBaChVRYO .relation{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-ZjWyhBxrBaChVRYO .dashed-line{stroke-dasharray:3;}#mermaid-svg-ZjWyhBxrBaChVRYO .dotted-line{stroke-dasharray:1 2;}#mermaid-svg-ZjWyhBxrBaChVRYO #compositionStart,#mermaid-svg-ZjWyhBxrBaChVRYO .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ZjWyhBxrBaChVRYO #compositionEnd,#mermaid-svg-ZjWyhBxrBaChVRYO .composition{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ZjWyhBxrBaChVRYO #dependencyStart,#mermaid-svg-ZjWyhBxrBaChVRYO .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ZjWyhBxrBaChVRYO #dependencyStart,#mermaid-svg-ZjWyhBxrBaChVRYO .dependency{fill:#333333!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ZjWyhBxrBaChVRYO #extensionStart,#mermaid-svg-ZjWyhBxrBaChVRYO .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ZjWyhBxrBaChVRYO #extensionEnd,#mermaid-svg-ZjWyhBxrBaChVRYO .extension{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ZjWyhBxrBaChVRYO #aggregationStart,#mermaid-svg-ZjWyhBxrBaChVRYO .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ZjWyhBxrBaChVRYO #aggregationEnd,#mermaid-svg-ZjWyhBxrBaChVRYO .aggregation{fill:transparent!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ZjWyhBxrBaChVRYO #lollipopStart,#mermaid-svg-ZjWyhBxrBaChVRYO .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ZjWyhBxrBaChVRYO #lollipopEnd,#mermaid-svg-ZjWyhBxrBaChVRYO .lollipop{fill:#ECECFF!important;stroke:#333333!important;stroke-width:1;}#mermaid-svg-ZjWyhBxrBaChVRYO .edgeTerminals{font-size:11px;line-height:initial;}#mermaid-svg-ZjWyhBxrBaChVRYO .classTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ZjWyhBxrBaChVRYO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ZjWyhBxrBaChVRYO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ZjWyhBxrBaChVRYO :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 实现(拿结果/取消)
实现(编排语法)
默认执行器(defaultExecutor)
<<interface>>
Future<T>
+get() : T
+cancel(boolean) : boolean
<<interface>>
CompletionStage<T>
+thenApply(fn)
+thenCompose(fn)
+thenCombine(other, fn)
+exceptionally(fn)
CompletableFuture<T>
-state 无锁完成状态
+supplyAsync(supplier) : <CF>
+runAsync(runnable) : <CF>
+allOf(cfs) : <CF>
+orTimeout(t, unit) : <CF>
ForkJoinPool
+commonPool
4. 扩展点
4.1 线程池隔离与合理配置
- 不要裸用
ForkJoinPool.commonPool():它被 JVM 内所有并行流、所有不带 executor 的 CF 共享,默认并行度 =CPU 核数 - 1,且线程是 daemon(main 不阻塞则进程直接退出);一个阻塞任务就可能饿死其他使用者; - 生产三条军规 :
- 编排任务的 IO(数据库/HTTP)一律
xxxAsync(fn, 业务线程池); - 业务池按"IO 型 + 有界队列 + 有名字"配置,拒绝策略明确(见 3.3 的
IO_POOL); - 池与业务生命周期一致(Spring 场景交给容器管理,见 4.3);
- 编排任务的 IO(数据库/HTTP)一律
- 常见错误:回调链里再
join/get阻塞(吃掉本池线程 → 池耗尽假死),见 FAQ Q8; - 并发知识总览见 \[Java多线程-知识大纲],无锁 CAS 细节见 \[ConcurrentHashMap源码解析]。
4.2 超时兜底 orTimeout
- JDK9+:
orTimeout让非阻塞链路 到点自行异常,配合exceptionally即"超时降级";completeOnTimeout直接填默认值走成功分支; - 调用顺序决定语义:
f.orTimeout(2,S).exceptionally(...)= 超时也降级;f.exceptionally(...).orTimeout(2,S)= 只在降级后再计时; - ⚠️ 只放行链路、不杀任务线程(见 FAQ Q11),真取消要协作式中断。
4.3 与 Spring @Async 结合
Spring 环境下尽量让容器管池子 ,并给 @Async 指定池:
yaml
# application.yml ------ Spring Boot 自定义异步池(@EnableAsync 后生效)
spring:
task:
execution:
pool:
core-size: 8
max-size: 32
queue-capacity: 500
thread-name-prefix: biz-async-
java
@Service
public class PriceService {
@Async("bizExecutor") // 指定 Bean 名,避免默认 SimpleAsyncTaskExecutor(每任务新建线程)
public CompletableFuture<BigDecimal> queryPrice(Long skuId) {
return CompletableFuture.completedFuture(doQuery(skuId)); // 实际执行者是 Spring 的 TaskExecutor
}
}
java
// Controller:编排在业务服务层,返回 CF,异步 Servlet 场景可把 CF 交给 Spring MVC 直接写响应
@GetMapping("/order/{id}")
public CompletableFuture<OrderVO> order(@PathVariable Long id) {
return priceSvc.queryPrice(id)
.thenCombine(stockSvc.queryStock(id), OrderVO::assemble)
.exceptionally(e -> OrderVO.fallback(e.getCause()));
}
⚠️ 注意:
@Async方法上 Spring 会把 CF 的完成回调调度在 TaskExecutor 上执行(走的是CompletableFuture.completedFuture+ 适配器),回调链默认接的是 commonPool------同样建议链上显式传池或把编排收在服务内部。
4.4 JDK 21 虚拟线程下的取舍
| 维度 | CompletableFuture | 虚拟线程 + 同步代码 |
|---|---|---|
| 代码形态 | 回调链/管道,声明式 | 传统阻塞式,逐行直读 |
| 排查体验 | 调用栈是执行器线程的,业务上下文断裂 | 栈就是业务逻辑,好调试 |
| IO 并发能力 | 线程池大小决定并发度 | 每请求一虚拟线程,量级到百万 |
| 适用 | 需要组合语义(竞速/聚合/超时旁路)、嵌入响应式链路 | 一请求多串行 IO,"并发收个结果"就够 |
实践建议(JDK21+):把"3 个 RPC 并行收结果"这类简单并发改成虚拟线程池 + 同步写法,代码量、正确性风险双降;CF 留给"结果要喂给异步组件/编排分支复杂/请求内管道"的场景:
java
// JDK21 替代示例:虚拟线程下的"并发收结果"甚至不需要 CF
ExecutorService vpool = Executors.newVirtualThreadPerTaskExecutor();
try (vpool) {
Future<Integer> a = vpool.submit(() -> rpc("价格服务", 500));
Future<Integer> b = vpool.submit(() -> rpc("库存服务", 300));
System.out.println(a.get() + b.get()); // 阻塞在虚拟线程上,代价可忽略
}
4.5 调试与观测
- 每步命名 :回调体内打
Thread.currentThread().getName()+ 业务标记,先定位"在哪条线程、哪个环节卡住/抛错"(3.3 示例里全程带线程名日志即是此意); - 终态兜底日志 :主链路末端统一挂
whenComplete((v, e) -> log),异常在潜伏期就可见; - 池监控 :
ThreadPoolExecutor暴露getActiveCount()/getQueue().size(),配 Spring Actuator 指标,饿死前有预警; - CF 没有自带的"链调试器",复杂链建议把大链拆成具名的小方法(
stage1()/stage2()),单步可测。
5. 常见问题 FAQ
Q1:get() 与 join() 有什么区别?
get()抛受检ExecutionException(要 try-catch),支持get(timeout)限时;join()抛非受检CompletionException,链式代码不用包 try。- 结论:编排链内统一用
join()(配合orTimeout做限时),代码最干净;需要受检异常语义或用旧 API 时用get()。
Q2:写了 supplyAsync 但程序直接退出了,任务根本没跑?
ForkJoinPool.commonPool 线程是 daemon ,main 不阻塞进程就结束了。解决:末端 join() 消费 + 自定义池要 shutdown()(见 3.3 结尾两行)。
Q3:thenApply 和 thenApplyAsync 到底差在哪?
差在执行线程:前者在上游"完成线程/注册线程"原地跑(不新起线程),后者丢给 commonPool/指定 executor。需要"链上每步都落业务池"就统一用 xxxAsync(fn, pool)。
Q4:exceptionally / handle / whenComplete 什么时候用哪个?
- 失败降级给默认值 →
exceptionally(只有异常才触发); - 终结点把成功失败统一转成一个结果 →
handle; - 只想打日志/埋点、不改结果 →
whenComplete。
whenComplete接不住异常------异常会继续往下传。
Q5:什么时候必须用 thenCompose 而不是 thenApply?
当映射函数返回的是另一个 CompletableFuture (如"查完用户再异步查订单")。用 thenApply 会得到 CompletableFuture<CompletableFuture<T>> 嵌套地狱。
Q6:链路里某一步抛异常了,为什么 log 里什么都没有?
因为 CF 异常是惰性传播 的:没人消费(不再挂方法或调用 join/get)就停留在状态里,不打印不抛出。解法见 4.5:末端挂 whenComplete 记日志,链路入口约定"每条链必须有终态消费者"。
Q7:不传 executor 行不行?有什么风险?
默认 ForkJoinPool.commonPool() 是 JVM 全局共享 + daemon:并行度只有 CPU-1,被并行流和其他 CF 共用,池里一个阻塞任务能饿死全局。并发编排属于 IO 型,务必自建池 (配置见 4.1)。
Q8:为什么服务假死?------回调里又去 join/get
最经典死锁:某线程执行回调时阻塞在 future.join(),而那个 future 的任务也排在同一池 → 池线程互相等。铁律:回调体内禁止阻塞等待同一池里的其他任务 ;需要等待就用 thenCompose 续链,让事件驱动而不是阻塞驱动。
Q9:allOf 返回 Void,怎么拿各个任务的结果?
各任务引用自留,allOf(...).thenApply(v -> futures.stream().map(CompletableFuture::join).toList())------此时已全部完成,join 只是取值不会阻塞(见 2.9 示例)。注意 2.9 示例里 Collectors.toList() 的 JDK 版本差异。
Q10:cancel(true) 能中断正在跑的任务吗?
不能 。cancel 只是把状态置为取消并让等待者立刻返回/抛 CancellationException,从不调用 Thread.interrupt()。要真停:任务内部定期检查 Thread.currentThread().isInterrupted(),由外部线程配合中断(协作式取消)。
Q11:orTimeout 超时后,那个慢任务还在跑吗?
在跑。超时只是让当前 CF 链以异常完成 、调用方放行,后台线程无人接管也不会被杀。若怕资源泄漏:任务内自实现"取消标记 + 超时后 cancel",或让任务自身支持中断。
Q12:JDK 21 都有虚拟线程了,还要学 CompletableFuture 吗?
场景分化(详见 4.4):简单"并发收结果"用虚拟线程同步代码更简单;需要组合语义 (竞速/聚合/条件分叉/超时旁路、要把异步结果喂给响应式框架或异步 Servlet)时 CF 仍是标准件,且虚拟线程完全可以把 CF 的阻塞消费 join() 变得廉价。两者共存而非替代。