Java 在 Controller 层使用 Callable 调用(提升效率)
高并发 Web 接口的响应效率,往往不是被「业务逻辑」拖慢的,而是被「线程被白白占用等待 IO」拖垮的。本文围绕 Java 在 Controller 层使用 Callable 展开两条实战主线:① Controller 返回 Callable,让 Spring MVC 异步处理请求、释放容器线程,提升吞吐量;② 在 Controller 里启动线程池,用 Callable 并发调用多个接口/服务,把串行等待变成并行聚合,降低接口 RT。全文包含可复制运行的 Spring Boot 代码、原理拆解、性能对比与踩坑指南。
适合人群:Java 后端开发(3 年以上经验)、Spring Boot 应用负责人、对接口性能优化感兴趣的同学。读完你将获得:理解 Web 容器线程模型 → 掌握 Controller 返回 Callable 的异步机制与配置 → 掌握线程池 + Callable + Future / CompletableFuture 并发聚合接口的完整套路 → 拿到超时、异常、TraceId 传递等生产级注意事项 → 一份可直接抄的速查表。
目录
入门与原理
- [一、为什么 Controller 需要 Callable:Web 线程模型与瓶颈](#一、为什么 Controller 需要 Callable:Web 线程模型与瓶颈)
- [二、Callable / Runnable / Future 三兄弟:异步任务基础](#二、Callable / Runnable / Future 三兄弟:异步任务基础)
- [三、Spring MVC 返回 Callable:异步请求处理原理](#三、Spring MVC 返回 Callable:异步请求处理原理)
- [四、配置异步支持:TaskExecutor 与超时设置](#四、配置异步支持:TaskExecutor 与超时设置)
三大实战
- [五、实战一:Controller 直接返回 Callable(异步接口)](#五、实战一:Controller 直接返回 Callable(异步接口))
- [六、实战二:线程池 + Callable 并发聚合多接口(串行变并行)](#六、实战二:线程池 + Callable 并发聚合多接口(串行变并行))
- [七、实战三:CompletableFuture 更优雅的并行聚合](#七、实战三:CompletableFuture 更优雅的并行聚合)
生产进阶
- 八、超时、异常与上下文传递(TraceId)
- [九、性能基准:串行 vs 并行、同步 vs 异步](#九、性能基准:串行 vs 并行、同步 vs 异步)
- 十、常见坑与最佳实践
- 十一、速查表
一、为什么 Controller 需要 Callable:Web 线程模型与瓶颈
1.1 Tomcat 的线程模型:一个请求占一个线程
绝大多数 Spring Boot 应用内嵌 Tomcat(默认 200 个工作线程)。当请求到达时,Tomcat 会从线程池里取一个线程来处理整个请求,直到响应写完才归还线程。
text
请求 A ──→ Tomcat 线程池 ──→ 线程1 处理请求A(阻塞在 IO 上...)
请求 B ──→ ├── 线程2 处理请求B(阻塞在 IO 上...)
请求 C ──→ ├── 线程3 处理请求C(阻塞在 IO 上...)
请求 D ──→ └── (线程池满,排队等待!)
问题在于:绝大多数 Controller 的耗时都在等待 IO ------ 查询数据库、调用其他微服务(RPC)、调第三方 API。这段时间线程既不算 CPU、也不干活,纯粹被占着。
1.2 两个经典瓶颈
| 瓶颈 | 场景 | 后果 |
|---|---|---|
| 吞吐量瓶颈 | 每个接口耗时 500ms(其中 450ms 在等 IO),Tomcat 200 线程 | 每秒最多处理 200 / 0.5s = 400 个请求,线程池打满后新请求只能排队,RT 雪崩 |
| 单请求 RT 瓶颈 | 订单详情接口要依次调 用户服务(100ms) → 商品服务(100ms) → 库存服务(100ms) | 串行耗时 300ms,其实这三个调用互不依赖,完全可以并行 |
1.3 Callable 的两条解决路径
Java 里 Callable 代表「一段可以返回结果、可抛异常、可在其他线程执行的任务」。围绕它,Controller 层有两种典型的提效姿势:
#mermaid-svg-oEGQ7N5EoqMcc9h9{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-oEGQ7N5EoqMcc9h9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .error-icon{fill:#552222;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .marker.cross{stroke:#333333;}#mermaid-svg-oEGQ7N5EoqMcc9h9 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-oEGQ7N5EoqMcc9h9 p{margin:0;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .cluster-label text{fill:#333;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .cluster-label span{color:#333;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .cluster-label span p{background-color:transparent;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .label text,#mermaid-svg-oEGQ7N5EoqMcc9h9 span{fill:#333;color:#333;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .node rect,#mermaid-svg-oEGQ7N5EoqMcc9h9 .node circle,#mermaid-svg-oEGQ7N5EoqMcc9h9 .node ellipse,#mermaid-svg-oEGQ7N5EoqMcc9h9 .node polygon,#mermaid-svg-oEGQ7N5EoqMcc9h9 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .rough-node .label text,#mermaid-svg-oEGQ7N5EoqMcc9h9 .node .label text,#mermaid-svg-oEGQ7N5EoqMcc9h9 .image-shape .label,#mermaid-svg-oEGQ7N5EoqMcc9h9 .icon-shape .label{text-anchor:middle;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .rough-node .label,#mermaid-svg-oEGQ7N5EoqMcc9h9 .node .label,#mermaid-svg-oEGQ7N5EoqMcc9h9 .image-shape .label,#mermaid-svg-oEGQ7N5EoqMcc9h9 .icon-shape .label{text-align:center;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .node.clickable{cursor:pointer;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .arrowheadPath{fill:#333333;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-oEGQ7N5EoqMcc9h9 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oEGQ7N5EoqMcc9h9 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-oEGQ7N5EoqMcc9h9 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .cluster text{fill:#333;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .cluster span{color:#333;}#mermaid-svg-oEGQ7N5EoqMcc9h9 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-oEGQ7N5EoqMcc9h9 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-oEGQ7N5EoqMcc9h9 rect.text{fill:none;stroke-width:0;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .icon-shape,#mermaid-svg-oEGQ7N5EoqMcc9h9 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .icon-shape p,#mermaid-svg-oEGQ7N5EoqMcc9h9 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .icon-shape .label rect,#mermaid-svg-oEGQ7N5EoqMcc9h9 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-oEGQ7N5EoqMcc9h9 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-oEGQ7N5EoqMcc9h9 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-oEGQ7N5EoqMcc9h9 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Controller 层用 Callable 提效
路径一:Controller 返回 Callable
路径二:Controller 内线程池 + Callable
Spring MVC 异步请求处理
容器线程立即释放
吞吐量大幅提升
并发调用多个下游接口
Future.get 汇总结果
单请求 RT 从串行变并行
- 路径一(异步接口) :
@GetMapping("/async") public Callable<String> async() { ... }。Spring MVC 检测到返回值是 Callable 后,把任务丢给异步线程池执行,Tomcat 线程立刻释放去处理其他请求。适合:接口本身耗时长(报表导出、批量计算)、高并发下防线程饥饿。 - 路径二(并发聚合) :在 Controller 方法体内,用
ExecutorService提交多个Callable,分别调用不同下游接口,再用Future.get()汇总。适合:一个接口要聚合多个无关的下游数据(BFF 层聚合、网关聚合)。
一句话总结:返回 Callable 是「把请求处理挪出容器线程」,线程池 + Callable 是「把多个串行 IO 变成并行」。二者可以组合使用,实战中经常一起出现。
二、Callable / Runnable / Future 三兄弟:异步任务基础
2.1 三种异步任务接口对比
| 特性 | Runnable | Callable<V> | Future<V> |
|---|---|---|---|
| 核心方法 | void run() |
V call() |
V get() |
| 是否有返回值 | ❌ 无 | ✅ 有(泛型) | ✅ 拿异步结果 |
| 能否抛受检异常 | ❌ 不能(只能包 RuntimeException) | ✅ 可以 throws Exception |
✅ get() 抛 ExecutionException |
| 能否取消 | 需自研标志 | 配合 Future | ✅ cancel() |
| 典型用途 | 日志、消息、无结果任务 | 有返回值的计算/IO 调用 | 获取异步任务结果 |
三者的协作关系可以概括为下图:
#mermaid-svg-3K42J7hd0fYuBJ6Y{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-3K42J7hd0fYuBJ6Y .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-3K42J7hd0fYuBJ6Y .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-3K42J7hd0fYuBJ6Y .error-icon{fill:#552222;}#mermaid-svg-3K42J7hd0fYuBJ6Y .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-3K42J7hd0fYuBJ6Y .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-3K42J7hd0fYuBJ6Y .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-3K42J7hd0fYuBJ6Y .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-3K42J7hd0fYuBJ6Y .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-3K42J7hd0fYuBJ6Y .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-3K42J7hd0fYuBJ6Y .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-3K42J7hd0fYuBJ6Y .marker{fill:#333333;stroke:#333333;}#mermaid-svg-3K42J7hd0fYuBJ6Y .marker.cross{stroke:#333333;}#mermaid-svg-3K42J7hd0fYuBJ6Y svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-3K42J7hd0fYuBJ6Y p{margin:0;}#mermaid-svg-3K42J7hd0fYuBJ6Y .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-3K42J7hd0fYuBJ6Y .cluster-label text{fill:#333;}#mermaid-svg-3K42J7hd0fYuBJ6Y .cluster-label span{color:#333;}#mermaid-svg-3K42J7hd0fYuBJ6Y .cluster-label span p{background-color:transparent;}#mermaid-svg-3K42J7hd0fYuBJ6Y .label text,#mermaid-svg-3K42J7hd0fYuBJ6Y span{fill:#333;color:#333;}#mermaid-svg-3K42J7hd0fYuBJ6Y .node rect,#mermaid-svg-3K42J7hd0fYuBJ6Y .node circle,#mermaid-svg-3K42J7hd0fYuBJ6Y .node ellipse,#mermaid-svg-3K42J7hd0fYuBJ6Y .node polygon,#mermaid-svg-3K42J7hd0fYuBJ6Y .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-3K42J7hd0fYuBJ6Y .rough-node .label text,#mermaid-svg-3K42J7hd0fYuBJ6Y .node .label text,#mermaid-svg-3K42J7hd0fYuBJ6Y .image-shape .label,#mermaid-svg-3K42J7hd0fYuBJ6Y .icon-shape .label{text-anchor:middle;}#mermaid-svg-3K42J7hd0fYuBJ6Y .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-3K42J7hd0fYuBJ6Y .rough-node .label,#mermaid-svg-3K42J7hd0fYuBJ6Y .node .label,#mermaid-svg-3K42J7hd0fYuBJ6Y .image-shape .label,#mermaid-svg-3K42J7hd0fYuBJ6Y .icon-shape .label{text-align:center;}#mermaid-svg-3K42J7hd0fYuBJ6Y .node.clickable{cursor:pointer;}#mermaid-svg-3K42J7hd0fYuBJ6Y .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-3K42J7hd0fYuBJ6Y .arrowheadPath{fill:#333333;}#mermaid-svg-3K42J7hd0fYuBJ6Y .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-3K42J7hd0fYuBJ6Y .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-3K42J7hd0fYuBJ6Y .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3K42J7hd0fYuBJ6Y .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-3K42J7hd0fYuBJ6Y .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3K42J7hd0fYuBJ6Y .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-3K42J7hd0fYuBJ6Y .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-3K42J7hd0fYuBJ6Y .cluster text{fill:#333;}#mermaid-svg-3K42J7hd0fYuBJ6Y .cluster span{color:#333;}#mermaid-svg-3K42J7hd0fYuBJ6Y 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-3K42J7hd0fYuBJ6Y .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-3K42J7hd0fYuBJ6Y rect.text{fill:none;stroke-width:0;}#mermaid-svg-3K42J7hd0fYuBJ6Y .icon-shape,#mermaid-svg-3K42J7hd0fYuBJ6Y .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-3K42J7hd0fYuBJ6Y .icon-shape p,#mermaid-svg-3K42J7hd0fYuBJ6Y .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-3K42J7hd0fYuBJ6Y .icon-shape .label rect,#mermaid-svg-3K42J7hd0fYuBJ6Y .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-3K42J7hd0fYuBJ6Y .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-3K42J7hd0fYuBJ6Y .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-3K42J7hd0fYuBJ6Y :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Runnable
Callable
创建 ExecutorService
提交任务
任务类型?
void run() 无返回值
V call() 有返回值 / 可抛异常
返回 Future(仅作任务句柄)
返回 Future(可取结果)
get() 阻塞同步结果
拿到结果 / 捕获 ExecutionException
2.2 从 Runnable 到 Callable:为什么必须用 Callable
java
// Runnable 版:拿不到返回值,异常只能内部吞掉
Runnable task = new Runnable() {
@Override
public void run() {
String result = userService.getUser(id); // 结果丢了!
// 抛异常?run() 不允许 throws,只能包成 RuntimeException
}
};
// Callable 版:返回值 + 异常都能抛给调用方
Callable<String> task = new Callable<String>() {
@Override
public String call() throws Exception {
return userService.getUser(id); // 结果通过 Future 拿
}
};
2.3 三种提交方式(可运行代码)
java
import java.util.concurrent.*;
public class CallableDemo {
static ExecutorService pool = Executors.newFixedThreadPool(3);
public static void main(String[] args) throws Exception {
// 方式一:提交给线程池,返回 Future
Future<String> f1 = pool.submit(() -> {
Thread.sleep(1000); // 模拟 IO
return "用户-张三";
});
System.out.println("f1 结果: " + f1.get()); // get() 阻塞等待结果
// 方式二:FutureTask 交给普通 Thread 跑(不用线程池)
FutureTask<Integer> ft = new FutureTask<>(() -> {
Thread.sleep(500);
return 42;
});
new Thread(ft).start();
System.out.println("ft 结果: " + ft.get());
// 方式三:Future.get(timeout) 带超时,防止无限等待
Future<String> f3 = pool.submit(() -> {
Thread.sleep(5000); // 模拟慢接口
return "慢接口结果";
});
try {
System.out.println("f3 结果: " + f3.get(2, TimeUnit.SECONDS));
} catch (TimeoutException e) {
System.out.println("f3 超时了!" + e.getMessage());
f3.cancel(true); // 主动取消
}
pool.shutdown(); // 优雅关闭
}
}
2.4 关键点:Future.get() 会阻塞当前线程
Future.get() 是阻塞式的:调用它的线程会一直等到任务完成。所以:
- 在主线程里
submit多个 Callable 后,先全部提交完,再逐个 get(),才能并行; - 如果提交一个就
get()一个,那又退回串行了(这是最常见的错误写法)。
java
// ❌ 错误:边提交边 get,实际是串行
Future<String> a = pool.submit(() -> serviceA());
String ra = a.get(); // 阻塞等 A 完成
Future<String> b = pool.submit(() -> serviceB());
String rb = b.get(); // 然后才等 B
// ✅ 正确:先全部提交,再统一 get
Future<String> a = pool.submit(() -> serviceA());
Future<String> b = pool.submit(() -> serviceB());
String ra = a.get(); // A、B 已经在并行执行
String rb = b.get();
三、Spring MVC 返回 Callable:异步请求处理原理
3.1 一条「同步接口」的完整生命周期
text
浏览器 ──→ Tomcat 线程1 ──→ Controller 业务逻辑(等 DB / 等 RPC)──→ 响应
↑________________ 这期间线程1 被占死 ________________|
Tomcat 线程 1 从收到请求到写完响应,全程被占用。业务逻辑等 IO 多久,线程就白等多久。
同步接口的线程占用可以用时序图直观表示:
Controller 业务 Tomcat 线程 客户端 Controller 业务 Tomcat 线程 客户端 #mermaid-svg-HM5S4VsfOROLbN4l{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-HM5S4VsfOROLbN4l .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HM5S4VsfOROLbN4l .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HM5S4VsfOROLbN4l .error-icon{fill:#552222;}#mermaid-svg-HM5S4VsfOROLbN4l .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HM5S4VsfOROLbN4l .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HM5S4VsfOROLbN4l .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HM5S4VsfOROLbN4l .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HM5S4VsfOROLbN4l .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HM5S4VsfOROLbN4l .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HM5S4VsfOROLbN4l .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HM5S4VsfOROLbN4l .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HM5S4VsfOROLbN4l .marker.cross{stroke:#333333;}#mermaid-svg-HM5S4VsfOROLbN4l svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HM5S4VsfOROLbN4l p{margin:0;}#mermaid-svg-HM5S4VsfOROLbN4l .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-HM5S4VsfOROLbN4l text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-HM5S4VsfOROLbN4l .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-HM5S4VsfOROLbN4l .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-HM5S4VsfOROLbN4l .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-HM5S4VsfOROLbN4l .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-HM5S4VsfOROLbN4l #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-HM5S4VsfOROLbN4l .sequenceNumber{fill:white;}#mermaid-svg-HM5S4VsfOROLbN4l #sequencenumber{fill:#333;}#mermaid-svg-HM5S4VsfOROLbN4l #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-HM5S4VsfOROLbN4l .messageText{fill:#333;stroke:none;}#mermaid-svg-HM5S4VsfOROLbN4l .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-HM5S4VsfOROLbN4l .labelText,#mermaid-svg-HM5S4VsfOROLbN4l .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-HM5S4VsfOROLbN4l .loopText,#mermaid-svg-HM5S4VsfOROLbN4l .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-HM5S4VsfOROLbN4l .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-HM5S4VsfOROLbN4l .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-HM5S4VsfOROLbN4l .noteText,#mermaid-svg-HM5S4VsfOROLbN4l .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-HM5S4VsfOROLbN4l .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-HM5S4VsfOROLbN4l .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-HM5S4VsfOROLbN4l .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-HM5S4VsfOROLbN4l .actorPopupMenu{position:absolute;}#mermaid-svg-HM5S4VsfOROLbN4l .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-HM5S4VsfOROLbN4l .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-HM5S4VsfOROLbN4l .actor-man circle,#mermaid-svg-HM5S4VsfOROLbN4l line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-HM5S4VsfOROLbN4l :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 线程被占死,IO 等待期间不算 CPU 发送请求 执行业务逻辑(等 DB / 等 RPC) 业务结果返回 写响应
3.2 Controller 返回 Callable 后发生了什么
当 Controller 方法返回 Callable<T> 时,Spring MVC 会走 Servlet 3.0 异步处理 (startAsync),把流程拆成两段:
Handler(业务逻辑) 异步任务线程池 Spring WebAsyncManager Tomcat容器线程 客户端 Handler(业务逻辑) 异步任务线程池 Spring WebAsyncManager Tomcat容器线程 客户端 #mermaid-svg-1nt3CVDOHvHyGVTA{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-1nt3CVDOHvHyGVTA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1nt3CVDOHvHyGVTA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1nt3CVDOHvHyGVTA .error-icon{fill:#552222;}#mermaid-svg-1nt3CVDOHvHyGVTA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1nt3CVDOHvHyGVTA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1nt3CVDOHvHyGVTA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1nt3CVDOHvHyGVTA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1nt3CVDOHvHyGVTA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1nt3CVDOHvHyGVTA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1nt3CVDOHvHyGVTA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1nt3CVDOHvHyGVTA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1nt3CVDOHvHyGVTA .marker.cross{stroke:#333333;}#mermaid-svg-1nt3CVDOHvHyGVTA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1nt3CVDOHvHyGVTA p{margin:0;}#mermaid-svg-1nt3CVDOHvHyGVTA .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-1nt3CVDOHvHyGVTA text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-1nt3CVDOHvHyGVTA .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-1nt3CVDOHvHyGVTA .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-1nt3CVDOHvHyGVTA .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-1nt3CVDOHvHyGVTA .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-1nt3CVDOHvHyGVTA #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-1nt3CVDOHvHyGVTA .sequenceNumber{fill:white;}#mermaid-svg-1nt3CVDOHvHyGVTA #sequencenumber{fill:#333;}#mermaid-svg-1nt3CVDOHvHyGVTA #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-1nt3CVDOHvHyGVTA .messageText{fill:#333;stroke:none;}#mermaid-svg-1nt3CVDOHvHyGVTA .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-1nt3CVDOHvHyGVTA .labelText,#mermaid-svg-1nt3CVDOHvHyGVTA .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-1nt3CVDOHvHyGVTA .loopText,#mermaid-svg-1nt3CVDOHvHyGVTA .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-1nt3CVDOHvHyGVTA .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-1nt3CVDOHvHyGVTA .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-1nt3CVDOHvHyGVTA .noteText,#mermaid-svg-1nt3CVDOHvHyGVTA .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-1nt3CVDOHvHyGVTA .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-1nt3CVDOHvHyGVTA .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-1nt3CVDOHvHyGVTA .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-1nt3CVDOHvHyGVTA .actorPopupMenu{position:absolute;}#mermaid-svg-1nt3CVDOHvHyGVTA .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-1nt3CVDOHvHyGVTA .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-1nt3CVDOHvHyGVTA .actor-man circle,#mermaid-svg-1nt3CVDOHvHyGVTA line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-1nt3CVDOHvHyGVTA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 发送请求 调用 Controller,拿到返回值 Callable 提交 Callable 到异步线程池 通知容器:请求已转为异步,线程可以释放 线程归还线程池,继续处理其他请求 ✅ 异步线程执行业务逻辑(等 DB / 等 RPC) 返回结果 回调通知结果就绪 dispatch 回容器线程池,完成响应 返回响应
关键点:
| 阶段 | 谁在执行 | 状态 |
|---|---|---|
| 请求进入 → 拿到 Callable | Tomcat 线程 | 毫秒级完成,随即释放 |
| 业务逻辑执行 | 异步任务线程池(TaskExecutor) | 业务跑多久都行,不占容器线程 |
| 结果返回 | Tomcat 线程 | 由容器 dispatch 回去完成响应 |
3.3 默认行为:一个必须知道的大坑
Spring MVC 默认使用 SimpleAsyncTaskExecutor 执行 Callable------它不限制并发,每个任务直接 new 一个线程!高并发下会无限创建线程,直接把机器拖垮。
⚠️ 生产环境必须 通过
WebMvcConfigurer#configureAsyncSupport换成有界线程池(见第四章)。
3.4 与 DeferredResult 的对比
| 特性 | Controller 返回 Callable | 返回 DeferredResult |
|---|---|---|
| 触发方式 | 由 Spring 的 TaskExecutor 自动执行 | 业务代码手动调用 setResult() 触发 |
| 适合场景 | 业务逻辑就在当前线程能算完(耗时操作) | 结果由另一个线程/另一个服务产生(如 MQ 回调、WebSocket 推送、长轮询) |
| 超时处理 | 配置默认超时,超时返回 503 | setErrorResult() / 超时回调 |
| 代码简洁度 | 高(一行 return) | 中(要持有 DeferredResult 对象) |
java
// DeferredResult 示例:结果由异步线程手动填充
@GetMapping("/deferred")
public DeferredResult<String> deferred() {
DeferredResult<String> result = new DeferredResult<>(5000L); // 5s 超时
asyncService.doHeavyJob(result); // 别的线程完成后 result.setResult("ok")
return result; // 当前线程立刻返回,容器线程释放
}
四、配置异步支持:TaskExecutor 与超时设置
4.1 必须配置的两件事
- 自定义 TaskExecutor :替换默认的
SimpleAsyncTaskExecutor,使用有界线程池; - 默认超时时间 :
Callable执行超过该时间,Spring 会中断任务并返回 503(容器默认超时一般 30s,可显式配置)。
4.2 Java 配置代码(可运行)
java
import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
public class AsyncConfig implements WebMvcConfigurer {
/** 专用于 Controller 返回 Callable 的异步任务线程池 */
@Bean("mvcTaskExecutor")
public Executor mvcTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8); // 核心线程数
executor.setMaxPoolSize(64); // 最大线程数
executor.setQueueCapacity(500); // 队列容量
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("mvc-async-");
// 拒绝策略:由调用线程(Tomcat 线程)直接执行,避免任务丢失
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true); // 优雅停机
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor;
}
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
configurer.setTaskExecutor(mvcTaskExecutor());
configurer.setDefaultTimeout(10_000); // 默认超时 10 秒(毫秒)
}
}
4.3 线程池参数怎么定(经验值)
| 参数 | 经验建议 |
|---|---|
| corePoolSize | CPU 核数 × 2 左右(任务大多是 IO 等待,可略大) |
| maxPoolSize | 一般为 core 的 4~8 倍,视下游能承受的并发而定 |
| queueCapacity | 200~1000,队列太长会掩盖问题,太短容易触发拒绝 |
| 拒绝策略 | 推荐 CallerRunsPolicy(降级为同步执行)或 AbortPolicy + 告警 |
| 线程名前缀 | 必须设置!排障时一眼认出异步线程 |
4.4 Spring Boot 配置项(application.yml)
yaml
spring:
mvc:
async:
request-timeout: 10000 # 异步请求超时(毫秒),等效 setDefaultTimeout
server:
tomcat:
threads:
max: 200 # Tomcat 容器线程数(默认 200)
min-spare: 20
accept-count: 200 # 等待队列长度
五、实战一:Controller 直接返回 Callable(异步接口)
5.1 完整可运行代码
java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.Callable;
@RestController
@RequestMapping("/api")
public class AsyncCallableController {
private static final Logger log = LoggerFactory.getLogger(AsyncCallableController.class);
/** 同步接口:全程占用 Tomcat 线程 */
@GetMapping("/sync")
public String sync(@RequestParam long sleepMs) throws InterruptedException {
log.info("[同步] 进入,线程 = {}", Thread.currentThread().getName());
Thread.sleep(sleepMs); // 模拟慢业务(DB / RPC)
log.info("[同步] 完成,线程 = {}", Thread.currentThread().getName());
return "sync done, slept " + sleepMs + "ms";
}
/** 异步接口:返回 Callable,容器线程立即释放 */
@GetMapping("/async")
public Callable<String> async(@RequestParam long sleepMs) {
log.info("[异步] 进入,容器线程 = {}", Thread.currentThread().getName());
return () -> {
log.info("[异步] 业务执行,异步线程 = {}", Thread.currentThread().getName());
Thread.sleep(sleepMs); // 慢业务在异步线程池执行
return "async done, slept " + sleepMs + "ms";
};
}
}
5.2 看日志理解线程切换
启动应用后依次访问:
bash
curl "http://localhost:8080/api/sync?sleepMs=3000"
curl "http://localhost:8080/api/async?sleepMs=3000"
观察控制台日志:
text
[同步] 进入,线程 = http-nio-8080-exec-1
[同步] 完成,线程 = http-nio-8080-exec-1 ← 同一个 Tomcat 线程,被占 3 秒
[异步] 进入,容器线程 = http-nio-8080-exec-2 ← 容器线程只干活 1ms
[异步] 业务执行,异步线程 = mvc-async-1 ← 慢业务在自定义线程池执行
同步与异步接口的线程流转对比如下:
#mermaid-svg-zULCy2UnXZ3nK10G{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-zULCy2UnXZ3nK10G .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-zULCy2UnXZ3nK10G .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-zULCy2UnXZ3nK10G .error-icon{fill:#552222;}#mermaid-svg-zULCy2UnXZ3nK10G .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-zULCy2UnXZ3nK10G .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-zULCy2UnXZ3nK10G .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-zULCy2UnXZ3nK10G .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-zULCy2UnXZ3nK10G .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-zULCy2UnXZ3nK10G .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-zULCy2UnXZ3nK10G .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-zULCy2UnXZ3nK10G .marker{fill:#333333;stroke:#333333;}#mermaid-svg-zULCy2UnXZ3nK10G .marker.cross{stroke:#333333;}#mermaid-svg-zULCy2UnXZ3nK10G svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-zULCy2UnXZ3nK10G p{margin:0;}#mermaid-svg-zULCy2UnXZ3nK10G .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-zULCy2UnXZ3nK10G .cluster-label text{fill:#333;}#mermaid-svg-zULCy2UnXZ3nK10G .cluster-label span{color:#333;}#mermaid-svg-zULCy2UnXZ3nK10G .cluster-label span p{background-color:transparent;}#mermaid-svg-zULCy2UnXZ3nK10G .label text,#mermaid-svg-zULCy2UnXZ3nK10G span{fill:#333;color:#333;}#mermaid-svg-zULCy2UnXZ3nK10G .node rect,#mermaid-svg-zULCy2UnXZ3nK10G .node circle,#mermaid-svg-zULCy2UnXZ3nK10G .node ellipse,#mermaid-svg-zULCy2UnXZ3nK10G .node polygon,#mermaid-svg-zULCy2UnXZ3nK10G .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-zULCy2UnXZ3nK10G .rough-node .label text,#mermaid-svg-zULCy2UnXZ3nK10G .node .label text,#mermaid-svg-zULCy2UnXZ3nK10G .image-shape .label,#mermaid-svg-zULCy2UnXZ3nK10G .icon-shape .label{text-anchor:middle;}#mermaid-svg-zULCy2UnXZ3nK10G .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-zULCy2UnXZ3nK10G .rough-node .label,#mermaid-svg-zULCy2UnXZ3nK10G .node .label,#mermaid-svg-zULCy2UnXZ3nK10G .image-shape .label,#mermaid-svg-zULCy2UnXZ3nK10G .icon-shape .label{text-align:center;}#mermaid-svg-zULCy2UnXZ3nK10G .node.clickable{cursor:pointer;}#mermaid-svg-zULCy2UnXZ3nK10G .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-zULCy2UnXZ3nK10G .arrowheadPath{fill:#333333;}#mermaid-svg-zULCy2UnXZ3nK10G .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-zULCy2UnXZ3nK10G .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-zULCy2UnXZ3nK10G .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zULCy2UnXZ3nK10G .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-zULCy2UnXZ3nK10G .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zULCy2UnXZ3nK10G .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-zULCy2UnXZ3nK10G .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-zULCy2UnXZ3nK10G .cluster text{fill:#333;}#mermaid-svg-zULCy2UnXZ3nK10G .cluster span{color:#333;}#mermaid-svg-zULCy2UnXZ3nK10G 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-zULCy2UnXZ3nK10G .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-zULCy2UnXZ3nK10G rect.text{fill:none;stroke-width:0;}#mermaid-svg-zULCy2UnXZ3nK10G .icon-shape,#mermaid-svg-zULCy2UnXZ3nK10G .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-zULCy2UnXZ3nK10G .icon-shape p,#mermaid-svg-zULCy2UnXZ3nK10G .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-zULCy2UnXZ3nK10G .icon-shape .label rect,#mermaid-svg-zULCy2UnXZ3nK10G .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-zULCy2UnXZ3nK10G .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-zULCy2UnXZ3nK10G .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-zULCy2UnXZ3nK10G :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 异步接口 /api/async
Tomcat 线程 http-nio-8080-exec-2 进入
拿到 Callable 后立即释放
mvc-async-1 异步线程执行 sleep(3000)
响应返回
同步接口 /api/sync
Tomcat 线程 http-nio-8080-exec-1 进入
sleep(3000) 占用 3 秒
响应返回,线程才归还
5.3 性能验证:并发压测对比
用 ab(Apache Bench)模拟 100 并发、每个请求 sleep 2 秒:
bash
ab -n 200 -c 100 "http://localhost:8080/api/sync?sleepMs=2000"
ab -n 200 -c 100 "http://localhost:8080/api/async?sleepMs=2000"
| 指标 | 同步接口 | Callable 异步接口 |
|---|---|---|
| 200 个请求总耗时 | 明显更长(容器线程被占,后续请求排队) | 更短(容器线程秒级释放,全并发执行) |
| Tomcat 线程占用峰值 | 200 线程全部被占 | 只有少量线程(用于接收/转发请求) |
| 线程饥饿风险 | 高 | 低(瓶颈转移到异步池,可控) |
关键认知:Callable 异步不能让单次请求变快,但它能显著提高系统的并发吞吐能力------同样的 Tomcat 线程数,能同时支撑的慢请求数量翻几倍甚至几十倍。
5.4 一个完整的业务示例:报表导出
java
@GetMapping("/report/export")
public Callable<ResponseEntity<byte[]>> exportReport(@RequestParam String date) {
return () -> {
// 这段重活(查库、聚合、生成 Excel)在异步线程执行
byte[] excel = reportService.generateExcel(date);
return ResponseEntity.ok()
.header("Content-Disposition", "attachment; filename=report.xlsx")
.body(excel);
};
}
六、实战二:线程池 + Callable 并发聚合多接口(串行变并行)
6.1 场景:订单详情聚合接口
一个 BFF(Backend For Frontend)接口要返回订单详情,需要聚合三个下游数据:
text
GET /api/order/{id} 需要:
① userService.getUser(userId) → 用户信息 100ms
② productService.getProduct(pid) → 商品信息 100ms
③ stockService.getStock(pid) → 库存信息 80ms
串行总耗时 ≈ 280ms,三个调用互不依赖 → 可以并行!
#mermaid-svg-HwH0BXLcbKQnyhB2{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-HwH0BXLcbKQnyhB2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HwH0BXLcbKQnyhB2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HwH0BXLcbKQnyhB2 .error-icon{fill:#552222;}#mermaid-svg-HwH0BXLcbKQnyhB2 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HwH0BXLcbKQnyhB2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HwH0BXLcbKQnyhB2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HwH0BXLcbKQnyhB2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HwH0BXLcbKQnyhB2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HwH0BXLcbKQnyhB2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HwH0BXLcbKQnyhB2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HwH0BXLcbKQnyhB2 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HwH0BXLcbKQnyhB2 .marker.cross{stroke:#333333;}#mermaid-svg-HwH0BXLcbKQnyhB2 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HwH0BXLcbKQnyhB2 p{margin:0;}#mermaid-svg-HwH0BXLcbKQnyhB2 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-HwH0BXLcbKQnyhB2 .cluster-label text{fill:#333;}#mermaid-svg-HwH0BXLcbKQnyhB2 .cluster-label span{color:#333;}#mermaid-svg-HwH0BXLcbKQnyhB2 .cluster-label span p{background-color:transparent;}#mermaid-svg-HwH0BXLcbKQnyhB2 .label text,#mermaid-svg-HwH0BXLcbKQnyhB2 span{fill:#333;color:#333;}#mermaid-svg-HwH0BXLcbKQnyhB2 .node rect,#mermaid-svg-HwH0BXLcbKQnyhB2 .node circle,#mermaid-svg-HwH0BXLcbKQnyhB2 .node ellipse,#mermaid-svg-HwH0BXLcbKQnyhB2 .node polygon,#mermaid-svg-HwH0BXLcbKQnyhB2 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HwH0BXLcbKQnyhB2 .rough-node .label text,#mermaid-svg-HwH0BXLcbKQnyhB2 .node .label text,#mermaid-svg-HwH0BXLcbKQnyhB2 .image-shape .label,#mermaid-svg-HwH0BXLcbKQnyhB2 .icon-shape .label{text-anchor:middle;}#mermaid-svg-HwH0BXLcbKQnyhB2 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-HwH0BXLcbKQnyhB2 .rough-node .label,#mermaid-svg-HwH0BXLcbKQnyhB2 .node .label,#mermaid-svg-HwH0BXLcbKQnyhB2 .image-shape .label,#mermaid-svg-HwH0BXLcbKQnyhB2 .icon-shape .label{text-align:center;}#mermaid-svg-HwH0BXLcbKQnyhB2 .node.clickable{cursor:pointer;}#mermaid-svg-HwH0BXLcbKQnyhB2 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-HwH0BXLcbKQnyhB2 .arrowheadPath{fill:#333333;}#mermaid-svg-HwH0BXLcbKQnyhB2 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HwH0BXLcbKQnyhB2 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HwH0BXLcbKQnyhB2 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HwH0BXLcbKQnyhB2 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-HwH0BXLcbKQnyhB2 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HwH0BXLcbKQnyhB2 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-HwH0BXLcbKQnyhB2 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-HwH0BXLcbKQnyhB2 .cluster text{fill:#333;}#mermaid-svg-HwH0BXLcbKQnyhB2 .cluster span{color:#333;}#mermaid-svg-HwH0BXLcbKQnyhB2 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-HwH0BXLcbKQnyhB2 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HwH0BXLcbKQnyhB2 rect.text{fill:none;stroke-width:0;}#mermaid-svg-HwH0BXLcbKQnyhB2 .icon-shape,#mermaid-svg-HwH0BXLcbKQnyhB2 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HwH0BXLcbKQnyhB2 .icon-shape p,#mermaid-svg-HwH0BXLcbKQnyhB2 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-HwH0BXLcbKQnyhB2 .icon-shape .label rect,#mermaid-svg-HwH0BXLcbKQnyhB2 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HwH0BXLcbKQnyhB2 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HwH0BXLcbKQnyhB2 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HwH0BXLcbKQnyhB2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 并行 100ms
用户服务
汇总
商品服务
库存服务
串行 280ms
用户服务 100ms
商品服务 100ms
库存服务 80ms
6.2 串行写法(问题代码)
java
@GetMapping("/order/serial/{id}")
public OrderDetailVO getOrderSerial(@PathVariable Long id) {
Order order = orderService.getOrder(id); // 50ms
UserVO user = userService.getUser(order.getUserId()); // 100ms
ProductVO product = productService.getProduct(order.getProductId()); // 100ms
StockVO stock = stockService.getStock(order.getProductId()); // 80ms
return OrderDetailVO.of(order, user, product, stock); // 总耗时 ≈ 330ms
}
6.3 并行写法:ExecutorService + Callable + Future
java
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.*;
import java.util.concurrent.*;
@RestController
@RequestMapping("/api")
public class OrderAggregateController {
/** 复用第四章配置的 mvcTaskExecutor(或单独建一个聚合专用线程池) */
private final ExecutorService pool;
public OrderAggregateController(@Qualifier("mvcTaskExecutor") Executor executor) {
// ThreadPoolTaskExecutor 本身实现了 ExecutorService 语义(实现 Executor 接口)
this.pool = (ExecutorService) executor;
}
@GetMapping("/order/parallel/{id}")
public OrderDetailVO getOrderParallel(@PathVariable Long id) throws Exception {
Order order = orderService.getOrder(id); // 串行:先拿到 order
// ① 先一次性提交三个互不依赖的 Callable
Future<UserVO> userFuture = pool.submit(() -> userService.getUser(order.getUserId()));
Future<ProductVO> productFuture = pool.submit(() -> productService.getProduct(order.getProductId()));
Future<StockVO> stockFuture = pool.submit(() -> stockService.getStock(order.getProductId()));
// ② 再统一取结果(带超时,防止下游拖垮本接口)
UserVO user = userFuture.get(800, TimeUnit.MILLISECONDS);
ProductVO product = productFuture.get(800, TimeUnit.MILLISECONDS);
StockVO stock = stockFuture.get(800, TimeUnit.MILLISECONDS);
return OrderDetailVO.of(order, user, product, stock); // 总耗时 ≈ 150ms
}
}
6.4 耗时对比(实测口径)
| 场景 | 总耗时 | 提升 |
|---|---|---|
| 串行调用(order + user + product + stock) | ~330ms | 基准 |
| 并行调用(submit 后统一 get) | ~150ms | RT 降低约 55% |
| 下游接口越多,收益越大 | 5 个下游:串行 500ms → 并行 ~120ms | 降低 75%+ |
并行提升的幅度取决于最慢的那个下游 (木桶效应):并行耗时 ≈
串行部分 + 最慢下游耗时。下游数量越多、单个越慢,并行收益越明显。
6.5 模拟三个慢接口的完整可运行示例
java
import org.springframework.stereotype.Service;
import java.util.concurrent.ThreadLocalRandom;
@Service
public class MockDownstreamService {
public String callUser() throws InterruptedException {
Thread.sleep(ThreadLocalRandom.current().nextInt(80, 120)); // 80~120ms
return "用户:张三";
}
public String callProduct() throws InterruptedException {
Thread.sleep(ThreadLocalRandom.current().nextInt(80, 120));
return "商品:MacBook Pro";
}
public String callStock() throws InterruptedException {
Thread.sleep(ThreadLocalRandom.current().nextInt(60, 100));
return "库存:66 件";
}
}
java
@RestController
@RequestMapping("/api")
public class AggregateDemoController {
private final MockDownstreamService mock;
private final ExecutorService pool;
public AggregateDemoController(MockDownstreamService mock,
@Qualifier("mvcTaskExecutor") Executor executor) {
this.mock = mock;
this.pool = (ExecutorService) executor;
}
/** 串行版 */
@GetMapping("/agg/serial")
public String serial() throws Exception {
long start = System.currentTimeMillis();
String u = mock.callUser();
String p = mock.callProduct();
String s = mock.callStock();
return "串行: " + (System.currentTimeMillis() - start) + "ms | " + u + " | " + p + " | " + s;
}
/** 并行版 */
@GetMapping("/agg/parallel")
public String parallel() throws Exception {
long start = System.currentTimeMillis();
Future<String> fu = pool.submit(mock::callUser);
Future<String> fp = pool.submit(mock::callProduct);
Future<String> fs = pool.submit(mock::callStock);
String u = fu.get(1, TimeUnit.SECONDS);
String p = fp.get(1, TimeUnit.SECONDS);
String s = fs.get(1, TimeUnit.SECONDS);
return "并行: " + (System.currentTimeMillis() - start) + "ms | " + u + " | " + p + " | " + s;
}
}
访问 http://localhost:8080/api/agg/serial 与 /api/agg/parallel,多刷几次对比耗时,串行约 240~340ms,并行约 90~120ms。
七、实战三:CompletableFuture 更优雅的并行聚合
7.1 为什么还要学 CompletableFuture
Future.get() 的痛点:
- 只能阻塞等待,没有回调;
- 多个 Future 无法方便地「组合」(全部完成、任一完成、聚合后再处理);
- 异常处理要在每个 get 处 try-catch。
CompletableFuture(JDK 8+)补齐了这些能力,且 supplyAsync 内部本质就是包了一层 Callable。
7.2 supplyAsync + allOf 聚合(等价于第六章并行版)
java
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.web.bind.annotation.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
@RestController
@RequestMapping("/api")
public class OrderAggregateCfController {
private final MockDownstreamService mock;
private final Executor executor; // 复用第四章配置的 mvcTaskExecutor
public OrderAggregateCfController(MockDownstreamService mock,
@Qualifier("mvcTaskExecutor") Executor executor) {
this.mock = mock;
this.executor = executor;
}
@GetMapping("/agg/cf")
public String cfAggregate() {
long start = System.currentTimeMillis();
// 三个任务并行提交(必须显式传线程池!否则默认用公共 ForkJoinPool)
CompletableFuture<String> fu = CompletableFuture.supplyAsync(mock::callUser, executor);
CompletableFuture<String> fp = CompletableFuture.supplyAsync(mock::callProduct, executor);
CompletableFuture<String> fs = CompletableFuture.supplyAsync(mock::callStock, executor);
// 全部完成后汇总(allOf 不抛异常,需要再 join 拿值)
CompletableFuture.allOf(fu, fp, fs).join();
return "CompletableFuture: " + (System.currentTimeMillis() - start) + "ms | "
+ fu.join() + " | " + fp.join() + " | " + fs.join();
}
}
CompletableFuture 的并行聚合流程如下:
#mermaid-svg-sFMvWEoBOBagI94g{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-sFMvWEoBOBagI94g .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sFMvWEoBOBagI94g .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sFMvWEoBOBagI94g .error-icon{fill:#552222;}#mermaid-svg-sFMvWEoBOBagI94g .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sFMvWEoBOBagI94g .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sFMvWEoBOBagI94g .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sFMvWEoBOBagI94g .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sFMvWEoBOBagI94g .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sFMvWEoBOBagI94g .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sFMvWEoBOBagI94g .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sFMvWEoBOBagI94g .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sFMvWEoBOBagI94g .marker.cross{stroke:#333333;}#mermaid-svg-sFMvWEoBOBagI94g svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sFMvWEoBOBagI94g p{margin:0;}#mermaid-svg-sFMvWEoBOBagI94g .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-sFMvWEoBOBagI94g .cluster-label text{fill:#333;}#mermaid-svg-sFMvWEoBOBagI94g .cluster-label span{color:#333;}#mermaid-svg-sFMvWEoBOBagI94g .cluster-label span p{background-color:transparent;}#mermaid-svg-sFMvWEoBOBagI94g .label text,#mermaid-svg-sFMvWEoBOBagI94g span{fill:#333;color:#333;}#mermaid-svg-sFMvWEoBOBagI94g .node rect,#mermaid-svg-sFMvWEoBOBagI94g .node circle,#mermaid-svg-sFMvWEoBOBagI94g .node ellipse,#mermaid-svg-sFMvWEoBOBagI94g .node polygon,#mermaid-svg-sFMvWEoBOBagI94g .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-sFMvWEoBOBagI94g .rough-node .label text,#mermaid-svg-sFMvWEoBOBagI94g .node .label text,#mermaid-svg-sFMvWEoBOBagI94g .image-shape .label,#mermaid-svg-sFMvWEoBOBagI94g .icon-shape .label{text-anchor:middle;}#mermaid-svg-sFMvWEoBOBagI94g .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-sFMvWEoBOBagI94g .rough-node .label,#mermaid-svg-sFMvWEoBOBagI94g .node .label,#mermaid-svg-sFMvWEoBOBagI94g .image-shape .label,#mermaid-svg-sFMvWEoBOBagI94g .icon-shape .label{text-align:center;}#mermaid-svg-sFMvWEoBOBagI94g .node.clickable{cursor:pointer;}#mermaid-svg-sFMvWEoBOBagI94g .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-sFMvWEoBOBagI94g .arrowheadPath{fill:#333333;}#mermaid-svg-sFMvWEoBOBagI94g .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-sFMvWEoBOBagI94g .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-sFMvWEoBOBagI94g .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sFMvWEoBOBagI94g .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-sFMvWEoBOBagI94g .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sFMvWEoBOBagI94g .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-sFMvWEoBOBagI94g .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-sFMvWEoBOBagI94g .cluster text{fill:#333;}#mermaid-svg-sFMvWEoBOBagI94g .cluster span{color:#333;}#mermaid-svg-sFMvWEoBOBagI94g 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-sFMvWEoBOBagI94g .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-sFMvWEoBOBagI94g rect.text{fill:none;stroke-width:0;}#mermaid-svg-sFMvWEoBOBagI94g .icon-shape,#mermaid-svg-sFMvWEoBOBagI94g .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-sFMvWEoBOBagI94g .icon-shape p,#mermaid-svg-sFMvWEoBOBagI94g .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-sFMvWEoBOBagI94g .icon-shape .label rect,#mermaid-svg-sFMvWEoBOBagI94g .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-sFMvWEoBOBagI94g .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-sFMvWEoBOBagI94g .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-sFMvWEoBOBagI94g :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Controller 入口
supplyAsync(mock::callUser, executor)
supplyAsync(mock::callProduct, executor)
supplyAsync(mock::callStock, executor)
CompletableFuture.allOf 等待全部完成
join() 提取结果
聚合返回
7.3 thenCombine:两个结果合并后再处理
java
// 场景:把「用户信息」和「商品信息」合并成一条展示文案
CompletableFuture<String> desc = fu.thenCombine(fp, (user, product) ->
"用户[" + user + "] 购买了 [" + product + "]");
System.out.println(desc.join());
7.4 异常处理:exceptionally / handle
java
CompletableFuture<String> safe = CompletableFuture
.supplyAsync(() -> mock.callUser(), executor)
.exceptionally(ex -> "用户服务异常,兜底返回: " + ex.getMessage());
// 或者 handle:无论成败都处理
CompletableFuture<String> handled = CompletableFuture
.supplyAsync(() -> mock.callProduct(), executor)
.handle((result, ex) -> ex == null ? result : "默认商品");
7.5 超时控制:orTimeout / completeOnTimeout(JDK 9+)
java
// 超时后抛异常
fu.orTimeout(800, TimeUnit.MILLISECONDS);
// 超时后用默认值兜底(推荐,不抛异常)
String name = fu.completeOnTimeout("用户-未知", 800, TimeUnit.MILLISECONDS).join();
7.6 ExecutorService + Future 与 CompletableFuture 怎么选
| 维度 | ExecutorService + Callable/Future | CompletableFuture |
|---|---|---|
| 复杂度 | 低,直观 | 中,API 丰富需要熟悉 |
| 组合编排 | 弱(只能逐个 get) | 强(allOf / anyOf / thenCombine / thenApply 链式) |
| 异常处理 | get() 抛 ExecutionException | exceptionally / handle |
| 超时 | get(timeout) | orTimeout / completeOnTimeout |
| 适用 | 简单「提交-汇总」 | 复杂依赖编排(A 完成后触发 B,多个结果合并等) |
八、超时、异常与上下文传递(TraceId)
8.1 超时:接口不能无限等下游
无论哪种写法,每个 Future 都要设超时,否则一个下游挂掉,整个接口卡死,线程池被占满。
java
// 方式一:Future.get(timeout)
try {
String r = future.get(800, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
log.warn("下游调用超时,走兜底逻辑");
r = "default";
future.cancel(true); // 中断任务(尽力而为)
}
// 方式二:CompletableFuture 兜底
String r = cf.completeOnTimeout("default", 800, TimeUnit.MILLISECONDS).join();
兜底值比抛异常更友好:接口返回「部分数据 + 缺失标记」,而不是直接 500。
超时处理的推荐决策流程如下:
#mermaid-svg-DEd7To0FK1kAUFFb{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-DEd7To0FK1kAUFFb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DEd7To0FK1kAUFFb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DEd7To0FK1kAUFFb .error-icon{fill:#552222;}#mermaid-svg-DEd7To0FK1kAUFFb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DEd7To0FK1kAUFFb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DEd7To0FK1kAUFFb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DEd7To0FK1kAUFFb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DEd7To0FK1kAUFFb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DEd7To0FK1kAUFFb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DEd7To0FK1kAUFFb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DEd7To0FK1kAUFFb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DEd7To0FK1kAUFFb .marker.cross{stroke:#333333;}#mermaid-svg-DEd7To0FK1kAUFFb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DEd7To0FK1kAUFFb p{margin:0;}#mermaid-svg-DEd7To0FK1kAUFFb .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-DEd7To0FK1kAUFFb .cluster-label text{fill:#333;}#mermaid-svg-DEd7To0FK1kAUFFb .cluster-label span{color:#333;}#mermaid-svg-DEd7To0FK1kAUFFb .cluster-label span p{background-color:transparent;}#mermaid-svg-DEd7To0FK1kAUFFb .label text,#mermaid-svg-DEd7To0FK1kAUFFb span{fill:#333;color:#333;}#mermaid-svg-DEd7To0FK1kAUFFb .node rect,#mermaid-svg-DEd7To0FK1kAUFFb .node circle,#mermaid-svg-DEd7To0FK1kAUFFb .node ellipse,#mermaid-svg-DEd7To0FK1kAUFFb .node polygon,#mermaid-svg-DEd7To0FK1kAUFFb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DEd7To0FK1kAUFFb .rough-node .label text,#mermaid-svg-DEd7To0FK1kAUFFb .node .label text,#mermaid-svg-DEd7To0FK1kAUFFb .image-shape .label,#mermaid-svg-DEd7To0FK1kAUFFb .icon-shape .label{text-anchor:middle;}#mermaid-svg-DEd7To0FK1kAUFFb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-DEd7To0FK1kAUFFb .rough-node .label,#mermaid-svg-DEd7To0FK1kAUFFb .node .label,#mermaid-svg-DEd7To0FK1kAUFFb .image-shape .label,#mermaid-svg-DEd7To0FK1kAUFFb .icon-shape .label{text-align:center;}#mermaid-svg-DEd7To0FK1kAUFFb .node.clickable{cursor:pointer;}#mermaid-svg-DEd7To0FK1kAUFFb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-DEd7To0FK1kAUFFb .arrowheadPath{fill:#333333;}#mermaid-svg-DEd7To0FK1kAUFFb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-DEd7To0FK1kAUFFb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-DEd7To0FK1kAUFFb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DEd7To0FK1kAUFFb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-DEd7To0FK1kAUFFb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DEd7To0FK1kAUFFb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-DEd7To0FK1kAUFFb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-DEd7To0FK1kAUFFb .cluster text{fill:#333;}#mermaid-svg-DEd7To0FK1kAUFFb .cluster span{color:#333;}#mermaid-svg-DEd7To0FK1kAUFFb 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-DEd7To0FK1kAUFFb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DEd7To0FK1kAUFFb rect.text{fill:none;stroke-width:0;}#mermaid-svg-DEd7To0FK1kAUFFb .icon-shape,#mermaid-svg-DEd7To0FK1kAUFFb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DEd7To0FK1kAUFFb .icon-shape p,#mermaid-svg-DEd7To0FK1kAUFFb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-DEd7To0FK1kAUFFb .icon-shape .label rect,#mermaid-svg-DEd7To0FK1kAUFFb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DEd7To0FK1kAUFFb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-DEd7To0FK1kAUFFb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-DEd7To0FK1kAUFFb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 否
是
调用 future.get(timeout)
是否超时?
拿到正常结果
捕获 TimeoutException
走兜底逻辑:返回默认值
future.cancel(true) 尽力中断
返回结果
8.2 异常处理:ExecutionException 需要解包
java
try {
String r = future.get();
} catch (ExecutionException e) {
// 任务内部抛出的异常被包在 ExecutionException 里,需要 unwrap
Throwable cause = e.getCause();
log.error("任务执行失败: {}", cause.getMessage(), cause);
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 恢复中断标志
log.error("线程被中断", e);
}
8.3 TraceId 传递:子线程丢上下文的经典坑
日志链路追踪(如 MDC 里的 TraceId)在线程切换后会丢失------子线程是全新的,不会继承父线程的 ThreadLocal/MDC。解决方案:自定义线程池,提交任务时把 MDC 快照带过去。
java
import org.slf4j.MDC;
import java.util.Map;
import java.util.concurrent.*;
/** 包装线程池:任务执行前恢复 MDC,执行后清理,实现 TraceId 跨线程传递 */
public class MdcThreadPoolExecutor extends ThreadPoolExecutor {
public MdcThreadPoolExecutor(int core, int max, long keepAlive,
TimeUnit unit, BlockingQueue<Runnable> queue,
ThreadFactory factory, RejectedExecutionHandler handler) {
super(core, max, keepAlive, unit, queue, factory, handler);
}
@Override
public void execute(Runnable command) {
super.execute(wrap(command, MDC.getCopyOfContextMap()));
}
@Override
public <T> Future<T> submit(Callable<T> task) {
return super.submit(wrapCallable(task, MDC.getCopyOfContextMap()));
}
private static Runnable wrap(Runnable task, Map<String, String> ctx) {
return () -> {
Map<String, String> previous = MDC.getCopyOfContextMap();
if (ctx == null) MDC.clear(); else MDC.setContextMap(ctx);
try {
task.run();
} finally {
if (previous == null) MDC.clear(); else MDC.setContextMap(previous);
}
};
}
private static <T> Callable<T> wrapCallable(Callable<T> task, Map<String, String> ctx) {
return () -> {
Map<String, String> previous = MDC.getCopyOfContextMap();
if (ctx == null) MDC.clear(); else MDC.setContextMap(ctx);
try {
return task.call();
} finally {
if (previous == null) MDC.clear(); else MDC.setContextMap(previous);
}
};
}
}
同理,用户身份、租户信息等 ThreadLocal 数据,也需要类似的包装传递(或用 Spring 的
TaskDecorator,见 8.4)。
8.4 Spring 官方推荐:TaskDecorator
java
@Bean("mvcTaskExecutor")
public Executor mvcTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// ... 其他参数同第四章 ...
executor.setTaskDecorator(runnable -> {
Map<String, String> ctx = MDC.getCopyOfContextMap();
return () -> {
Map<String, String> prev = MDC.getCopyOfContextMap();
if (ctx == null) MDC.clear(); else MDC.setContextMap(ctx);
try {
runnable.run();
} finally {
if (prev == null) MDC.clear(); else MDC.setContextMap(prev);
}
};
});
executor.initialize();
return executor;
}
8.5 优雅关闭:应用停机时不能丢任务
java
@PreDestroy
public void shutdownPool() {
pool.shutdown(); // 拒绝新任务,等待已提交任务完成
try {
if (!pool.awaitTermination(30, TimeUnit.SECONDS)) {
pool.shutdownNow(); // 超时强制中断
}
} catch (InterruptedException e) {
pool.shutdownNow();
Thread.currentThread().interrupt();
}
}
使用
ThreadPoolTaskExecutor时,setWaitForTasksToCompleteOnShutdown(true)+setAwaitTerminationSeconds(30)已有内置优雅停机支持。
九、性能基准:串行 vs 并行、同步 vs 异步
9.1 接口聚合:串行 vs 并行(下游数量递增)
假设每个下游接口耗时 100ms:
#mermaid-svg-qTx529rNn3rrhFsk{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-qTx529rNn3rrhFsk .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-qTx529rNn3rrhFsk .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-qTx529rNn3rrhFsk .error-icon{fill:#552222;}#mermaid-svg-qTx529rNn3rrhFsk .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-qTx529rNn3rrhFsk .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-qTx529rNn3rrhFsk .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-qTx529rNn3rrhFsk .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-qTx529rNn3rrhFsk .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-qTx529rNn3rrhFsk .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-qTx529rNn3rrhFsk .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-qTx529rNn3rrhFsk .marker{fill:#333333;stroke:#333333;}#mermaid-svg-qTx529rNn3rrhFsk .marker.cross{stroke:#333333;}#mermaid-svg-qTx529rNn3rrhFsk svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-qTx529rNn3rrhFsk p{margin:0;}#mermaid-svg-qTx529rNn3rrhFsk :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 串行 vs 并行 RT 对比(每个下游 100ms) 2个下游 3个下游 5个下游 8个下游 900 800 700 600 500 400 300 200 100 0 耗时 ms
| 下游数量 | 串行耗时 | 并行耗时 | 优化幅度 |
|---|---|---|---|
| 2 个 | 200ms | ~105ms | 47% |
| 3 个 | 300ms | ~110ms | 63% |
| 5 个 | 500ms | ~115ms | 77% |
| 8 个 | 800ms | ~125ms | 84% |
结论:下游越多、单个越慢,并行收益越大。并行耗时 ≈ 串行前置 + 最慢下游耗时 + 汇总开销。
9.2 异步接口:同步 vs Callable(Tomcat 200 线程,接口耗时 500ms)
| 指标 | 同步接口 | Callable 异步接口(异步池 64 线程) |
|---|---|---|
| 可同时处理的慢请求数 | 200(受容器线程限制) | 200 接收 + 64 并行执行,排队深度更可控 |
| 线程饥饿 | 容器线程打满即雪崩 | 容器线程秒级释放,几乎不饥饿 |
| 超时慢请求的处理 | 一直占着线程 | 10s 超时后中断释放线程 |
| 单请求 RT | 不变 | 不变(但整体吞吐显著提升) |
一句话:同步接口的并发上限 = 容器线程数;异步接口的并发上限 = 异步线程池大小(可控、可调)。生产上把慢接口改成 Callable,配合压测调好线程池,是性价比极高的优化。
9.3 压测工具建议
bash
# ab:简单场景
ab -n 2000 -c 200 "http://localhost:8080/api/async?sleepMs=500"
# wrk:更贴近真实(支持脚本)
wrk -t8 -c200 -d30s "http://localhost:8080/api/async?sleepMs=500"
对比指标:Requests/sec(吞吐)、Failed requests(失败数)、95% 响应时间。
十、常见坑与最佳实践
10.1 八大常见坑
| # | 坑 | 后果 | 解法 |
|---|---|---|---|
| 1 | 用默认 SimpleAsyncTaskExecutor | 无限 new 线程,内存/CPU 耗尽 | 必须换成有界 ThreadPoolTaskExecutor |
| 2 | Future.get() 不设超时 |
下游挂掉 → 接口无限阻塞 → 线程池占满 | 一律 get(timeout) 或 completeOnTimeout |
| 3 | 边 submit 边 get | 实际退化成串行,白开线程池 | 先全部提交,再统一 get |
| 4 | CompletableFuture 不传线程池 | 默认用公共 ForkJoinPool,易被其他任务干扰 | 显式传 executor 参数 |
| 5 | TraceId / 用户信息没传子线程 | 日志断链、权限校验失败 | TaskDecorator / 包装线程池传递 MDC |
| 6 | 事务边界错误 | 异步线程里没有原事务,数据不一致 | 异步任务内自己开事务,或避免事务跨越异步 |
| 7 | 线程池未优雅关闭 | 停机时丢失任务 / 线程泄漏 | @PreDestroy shutdown + awaitTermination |
| 8 | 在 Callable 里访问「共享的可变状态」 | 并发修改异常、脏数据 | 只传参数,不用共享字段;必须共享则加锁/原子类 |
10.2 关于事务的特别提醒
java
// ❌ 危险:事务方法内提交异步任务,事务上下文不会带到子线程
@Transactional
public void doBiz() {
executor.submit(() -> {
dao.update(...); // 这个更新不在外面的事务里!
});
}
异步线程与调用方不在同一个事务。需要事务的异步操作,应在任务内部自己声明事务(如自建 Service 方法加 @Transactional),或改用消息队列等异步方案。
10.3 关于 Callable 里使用 HttpServletRequest
Controller 返回 Callable 后,Spring 会把请求相关的 RequestAttributes 绑定到异步执行线程,因此在 Callable 中通过 RequestContextHolder 拿 Request 通常是可以的(Spring 内部做了传递):
java
@GetMapping("/async-ctx")
public Callable<String> asyncCtx() {
return () -> {
// 能拿到请求头(Spring 异步处理会恢复 RequestAttributes)
String token = ((ServletRequestAttributes) RequestContextHolder
.getRequestAttributes()).getRequest().getHeader("Authorization");
return "token = " + token;
};
}
但要避免在异步线程里直接持有
HttpServletResponse做流式操作,写响应应交给 Spring 回调机制(返回结果即可)。
10.4 最佳实践清单(发布前自查)
- 异步 TaskExecutor 用的是自定义有界线程池,而不是默认 SimpleAsyncTaskExecutor
- 每个
Future.get()都设置了超时,并有兜底值 - 线程池参数经过压测(core/max/queue 与下游容量匹配)
- TraceId、用户信息等 ThreadLocal 通过 TaskDecorator 传递
- 聚合调用先全部 submit 再统一 get(真并行)
- CompletableFuture 全部显式传了 executor
- 应用停机时线程池优雅关闭(waitForTasksToCompleteOnShutdown)
- 慢接口已从同步改为返回 Callable / DeferredResult,并验证吞吐提升
- 加了线程名前缀(mvc-async-),日志排障友好
十一、速查表
11.1 核心类与接口
| 类 / 接口 | 作用 | 关键方法 |
|---|---|---|
Callable<V> |
有返回值的异步任务 | V call() throws Exception |
Runnable |
无返回值任务 | void run() |
Future<V> |
异步结果容器 | get() / get(timeout) / cancel() / isDone() |
FutureTask<V> |
Runnable + Future 实现 | 可直接 new Thread(task).start() |
ExecutorService |
线程池提交任务 | submit(Callable) / invokeAll / shutdown |
CompletableFuture<V> |
可编排的异步结果 | supplyAsync / allOf / thenCombine / exceptionally / orTimeout |
ThreadPoolTaskExecutor |
Spring 线程池(推荐) | setCorePoolSize / setTaskDecorator / setWaitForTasksToCompleteOnShutdown |
AsyncSupportConfigurer |
配置 MVC 异步支持 | setTaskExecutor / setDefaultTimeout |
11.2 一行速记
java
// ① Controller 返回 Callable ------ 释放容器线程
public Callable<String> api() { return () -> doHeavy(); }
// ② 线程池提交 Callable ------ 并行聚合
Future<String> f = pool.submit(() -> service.call());
// ③ 带超时取结果 ------ 防止拖垮接口
String r = f.get(800, TimeUnit.MILLISECONDS);
// ④ CompletableFuture ------ 并行 + 兜底
String r = CompletableFuture
.supplyAsync(() -> service.call(), executor)
.completeOnTimeout("default", 800, TimeUnit.MILLISECONDS)
.join();
11.3 常用配置速查(application.yml)
yaml
spring:
mvc:
async:
request-timeout: 10000 # 异步请求默认超时(ms)
server:
tomcat:
threads:
max: 200 # 容器线程数
accept-count: 200 # 等待队列
至此,你已经掌握了 Java 在 Controller 层使用 Callable 的两条提效主线:返回 Callable 让容器线程秒级释放、提升吞吐 ;线程池 + Callable/Future(或 CompletableFuture)把串行聚合变并行、降低 RT 。两者的共同前提是:有界线程池、统一超时、上下文传递、优雅关闭。把这套组合拳用起来,你的接口在高并发下会稳得多。祝编码顺利!