---
title: "从 ExecutorConfig 看 Java 后端并发"
date: 2026-06-23
description: "Spring Boot 中 HTTP 请求由 Tomcat 工作线程同步处理，耗时任务会占满有限线程池导致 RT 飙升。ExecutorConfig 将任务剥离至专用线程池异步执行——虽仅 40 余行，却串联 Spring IoC、AOP、JUC 线程池、拒绝策略、优雅停机与可观测性。本文结合源码与生产实践做系统复盘。"
canonical_url: "https://yijinlee.com/articles/article-57"
tags:
  - Java
  - "Spring Boot"
  - "@Async"
  - ThreadPoolExecutor
  - ThreadPoolTaskExecutor
  - 并发编程
  - 拒绝策略
  - 优雅停机
topic: "架构之道"
---

METRICS: 8 模块 · 4 拒绝策略 · 150 槽位 · 40 行配置

在 Spring Boot 服务中，HTTP 请求默认由 Tomcat 的工作线程（`http-nio-exec-*`）处理。若业务中混有发消息、调外部 API、批量数据计算等耗时操作，且这些操作同步执行，请求线程将被长期阻塞。Tomcat 线程池大小有限（默认 200），占满后新请求排队或超时，表现为接口 RT 升高，甚至服务不可用。

**ExecutorConfig 的职责便是将耗时任务从请求线程剥离，交由专用线程池异步执行。** 这一配置类虽仅 40 余行，背后却串联了 Spring IoC、AOP、JDK 线程池实现、拒绝策略、优雅停机及可观测性等若干底层技术。下文结合源码与生产实践，做一次系统复盘。

PARAMS:
core|5|低负载常驻，减少上下文切换
max|100|高负载最多 100 线程并行
queue|50|有界队列缓冲待执行任务
keepAlive|60s|非核心线程空闲回收

### 一、Spring 容器托管：从 new 到 Bean 生命周期

`@Configuration` 标记的类会被 CGLIB 增强，确保其中 `@Bean` 方法返回的单例对象由容器管理。`ThreadPoolTaskExecutor` 实例注册到 IoC 容器后，所有组件可通过 `@Autowired` 或 `@Async("asyncTaskExecutor")` 引用，避免了各处 `new ThreadPoolExecutor()` 导致资源失控。

`@EnableAsync` 引入 `AsyncConfigurationSelector`，最终向容器注册 `ProxyAsyncConfiguration`，该配置通过 `AsyncAnnotationBeanPostProcessor` 为标注了 `@Async` 的 Bean 创建代理。代理默认使用 Spring 的 Advisor 机制，通过 `AnnotationAsyncExecutionInterceptor` 拦截方法调用，将方法体包装为 `Callable` 提交给指定 Executor，调用方立即返回。

NOTE: **@Async 基于代理，同类内部方法调用不会经过代理对象，因此异步不会生效。** 这是 Spring AOP 的经典限制，与 `@Transactional` 同理。

```text
// 伪代码 · DemoApp 服务异步线程池
[配置类] AsyncPoolConfig
  启用: @EnableAsync

  [Bean] asyncTaskExecutor():
    pool = 创建线程池(
      corePoolSize:   5,
      maxPoolSize:    100,
      queueCapacity:  50,
      keepAlive:      60s,
      threadPrefix:   "DemoApp-Async-",
      rejectPolicy:   CallerRunsPolicy,   // 过载 → 调用方线程执行（背压）
      gracefulShutdown: {
        waitForTasks: true,
        timeout:      60s
      }
    )
    return pool.init()
```

### 二、JDK 线程池核心：ThreadPoolExecutor 的调度算法

`ThreadPoolTaskExecutor` 是对 `java.util.concurrent.ThreadPoolExecutor` 的封装。理解参数必须理解 `execute` 方法的执行流程（JDK 17 源码简化）：

```java
public void execute(Runnable command) {
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true)) return;
        c = ctl.get();
    }
    if (isRunning(c) && workQueue.offer(command)) {
        // 再次检查状态，失败则 reject
    } else if (!addWorker(command, false)) {
        reject(command);
    }
}
```

RULES:
1|workerCount < corePoolSize|新建核心线程执行
2|否则 workQueue.offer|成功则入队等待核心线程消费
3|入队失败且线程数 < maximumPoolSize|新建非核心线程执行
4|队列满且线程数已达上限|触发拒绝策略

理论处理槽位为 **max + queue = 150**（实际运行任务数最多 100，等待 50）。有界队列选用 `LinkedBlockingQueue` 并显式指定容量，这是防止任务无限堆积导致 OOM 的关键。无界队列（如 `newFixedThreadPool` 默认容量为 `Integer.MAX_VALUE`）会使得 `maximumPoolSize` 参数形同虚设，线程数永远不会超过 `corePoolSize`，内存风险高。

### 三、拒绝策略：背压与降级的权衡

```java
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
```

POLICIES:
AbortPolicy|抛出 RejectedExecutionException|默认策略，要求调用方显式处理失败
CallerRunsPolicy|提交线程直接执行|背压机制，过载时将压力反向传递给调用方
DiscardPolicy|静默丢弃任务|无感知丢失，生产环境慎用
DiscardOldestPolicy|丢弃队列最旧任务|腾出空间再尝试提交当前任务

NOTE: **CallerRunsPolicy 本质上是一种背压机制**：当线程池过载时，将压力反向传递给调用方。若调用方是 HTTP 请求线程，则该请求会被异步任务拖慢，从而降低接收新请求的速率，达到保护系统整体稳定的目的——这一思想与 Reactive Streams 中的 `onBackpressure` 同源。选择何种策略，本质是业务容忍度与系统稳定性的折中。

### 四、优雅停机：容器生命周期与线程池的协同

```java
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(60);
```

当 Spring 容器关闭（如 K8s 滚动更新、应用收到 SIGTERM），`ThreadPoolTaskExecutor` 的 `destroy()` 方法被调用：

- 调用 `shutdown()`，停止接收新任务，并中断空闲线程。
- 若 `waitForTasksToCompleteOnShutdown` 为 true，则等待当前执行中的任务完成，最长等待 `awaitTerminationSeconds` 秒。
- 超时后调用 `shutdownNow()`，尝试中断正在执行的任务（通过 `Thread.interrupt()`）。

NOTE: `shutdown()` 将线程池状态置为 SHUTDOWN，拒绝新任务但继续执行队列中的任务；`shutdownNow()` 将状态置为 STOP 并尝试中断所有工作线程。优雅停机需要业务代码对中断响应（`Thread.interrupted()` 检查），若任务中执行了阻塞 I/O 或 sleep，需正确处理 `InterruptedException` 以确保资源释放。

### 五、可观测性：线程命名与链路追踪

```text
// 伪代码
pool.threadPrefix = "DemoApp-Async-"
```

线程 Dump、日志、APM 中，`DemoApp-Async-3` 可清晰区分业务异步线程与 Tomcat 工作线程。在生产排查 CPU 飙高、死锁或阻塞时，命名前缀是低成本高回报的手段。

更进一步的，结合 `TaskDecorator` 可以自动传递 MDC 上下文（如 TraceId），使得异步任务的日志也能关联到原始请求的调用链，这在微服务分布式追踪中尤为重要。

### 六、@Async 细节与异常处理

#### Bean 名称契约

`@Bean` 方法返回 `Executor`，默认 Bean 名即为方法名 `asyncTaskExecutor`。使用 `@Async("asyncTaskExecutor")` 必须与 Bean 名严格匹配，否则 Spring 会按 default 寻找 `taskExecutor`，失败或误用。

#### 返回值语义

`@Async` 方法返回 `void` 时，调用方无法获取执行结果或异常。未捕获的异常由 `AsyncUncaughtExceptionHandler` 处理（默认只打日志）。若需异步结果与异常回调，应返回 `Future` 或 `CompletableFuture`，并配合 `exceptionally()` / `whenComplete()` 处理。

#### 代理机制

Spring 对 `@Async` 默认使用 CGLIB 代理（若目标类实现了接口，也可使用 JDK 动态代理）。代理类在调用目标方法前，通过拦截器将其提交给线程池，而非同步执行。

### 七、实践中的关键追问

#### 为何不推荐 Executors 工厂方法？

- `newFixedThreadPool` 使用无界队列，`maximumPoolSize` 无效，任务堆积可致 OOM。
- `newCachedThreadPool` 线程数无上限，高并发下创建大量线程导致系统资源耗尽。
- 显式 `ThreadPoolTaskExecutor` + 有界参数是可控的方案。

#### ThreadPoolTaskExecutor 与裸 ThreadPoolExecutor 的差异？

- Spring 封装了 `initialize()` 和 `destroy()`，与容器生命周期集成。
- 支持 `TaskDecorator` 用于上下文传递（如 MDC）。
- 默认使用 `ThreadPoolExecutor` 的 `allowCoreThreadTimeOut` 可配置。

#### 如何评估参数是否合理？

需结合业务 QPS、任务平均耗时、任务类型（CPU 密集 vs I/O 密集）。通常：

- **CPU 密集型**：`corePoolSize = CPU 核数 + 1`，`maxPoolSize ≈ core`。
- **I/O 密集型**：可设置更大值（如 `2 × CPU 核数`），并配合较短的 `keepAliveTime`。

实践中需通过压测验证队列长度与拒绝率，动态调整。

### 八、总结

PILLARS:
Spring 容器|统一管理线程池，避免散乱；通过 AOP 实现异步解耦
JUC 线程池|有界队列、饱和策略、Worker 线程复用，构成稳定可靠的任务执行框架
工程化实践|优雅停机保障数据一致性，线程命名增强可观测性，拒绝策略实现背压控制

真正理解这段配置，需深入 `ThreadPoolExecutor` 源码，理解其状态机（RUNNING、SHUTDOWN、STOP、TIDYING、TERMINATED）和 Worker 的锁机制（`ReentrantLock` + `Condition`）。同时，要能够结合业务场景，权衡性能、可靠性、资源消耗三者的关系，而非盲目照搬模板。

## 参考文章：相关链接

- [Spring · Task Execution and Scheduling](https://docs.spring.io/spring-framework/reference/integration/scheduling.html)（官方文档）— Spring 任务执行与调度官方文档，对照 ExecutorConfig。
- [Oracle · Java Concurrency Utilities](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/package-summary.html)（权威引文）— JUC 线程池与拒绝策略 API，理解 40 行配置背后的并发模型。
- [AI 场景助手的前端架构现实主义演进](https://yijinlee.com/articles/article-58)（站内深度）— 前后端各自用「现实主义」治理复杂度。
- [深圳带娃前端的跨界破壁指南](https://yijinlee.com/articles/article-22)（站内深度）— 前端进入 Java 并发的动机篇。

