接口调用性能优化——Java 在 Controller 层使用 Callable 调用(提升效率)

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 更优雅的并行聚合)

生产进阶


一、为什么 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 必须配置的两件事

  1. 自定义 TaskExecutor :替换默认的 SimpleAsyncTaskExecutor,使用有界线程池;
  2. 默认超时时间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 。两者的共同前提是:有界线程池、统一超时、上下文传递、优雅关闭。把这套组合拳用起来,你的接口在高并发下会稳得多。祝编码顺利!

相关推荐
墨雨晨曦881 小时前
2026/08/15 spring AI学习总结
java·tomcat
wno7042 小时前
Spring Boot JdbcTemplate配置Druid多数据源
java·spring boot·后端
ShiXZ2132 小时前
网络调试四剑客:ping / telnet / nc / netstat 速查指令集
运维·开发语言·网络·php
码农大叔的博客2 小时前
golang示例:switch
开发语言·后端·golang
个 人 练 习 生2 小时前
数据结构入门:算法复杂度
开发语言·数据结构·经验分享·学习·程序人生·算法
W_326002 小时前
Python-OpenCV边缘检测与阈值分割:Sobel、Scharr、Laplacian、Canny、全局与自适应阈值
开发语言·图像处理·python·opencv·机器学习
其实防守也摸鱼2 小时前
红队技能总结导图:从入门到精通的完整知识体系
开发语言·人工智能·学习·安全·web安全
聊浮游2 小时前
JAVA2026最新全套学习资料、学习路线
java·开发语言·jvm·mysql·spring·maven·idea
MacroZheng2 小时前
完美替代 Navicat!这款内置 AI 的数据库工具,太香了!
java·后端·mysql
SamDeepThinking3 小时前
从REST到gRPC,一个API选型的思考框架
java·后端·程序员