Spring Boot 3 + JDK 17:线程池创建与使用完全指南
环境基线:JDK 17 、Spring Boot 3.x (对应 Spring Framework 6.x)。
Spring Boot 3.0 起最低要求就是 Java 17,本文所有代码均可直接在该环境运行。
一、为什么必须用线程池
直接 new Thread() 的问题人尽皆知:线程创建/销毁开销大、无法控制并发总量、出问题难以排查。线程池的价值在于三点:
- 复用:核心线程常驻,避免频繁创建销毁;
- 管控:用有界队列和拒绝策略给系统加一道保险,防止资源被无限吃光;
- 可观测:池本身暴露队列深度、活跃线程数等指标,方便监控和调优。
在 Spring Boot 3 项目里,创建线程池主要有三条路线:
| 路线 | 适用场景 |
|---|---|
JDK 原生 ThreadPoolExecutor |
需要完全掌控每个参数、或脱离 Spring 容器的组件 |
Spring ThreadPoolTaskExecutor + @Async |
业务异步方法、需要和 Spring 生命周期/上下文集成 |
| Spring Boot 自动配置属性 | 快速配置全局默认异步线程池 |
下面逐一展开。
二、先搞懂 ThreadPoolExecutor 的 7 个参数
无论哪条路线,最终干活的都是 java.util.concurrent.ThreadPoolExecutor,它的构造函数是理解一切线程池的基础:
java
public ThreadPoolExecutor(
int corePoolSize, // 1. 核心线程数:常驻线程(允许核心线程超时回收除外)
int maximumPoolSize, // 2. 最大线程数:队列满后线程数可膨胀到的上限
long keepAliveTime, // 3. 非核心线程空闲存活时间
TimeUnit unit, // 4. 存活时间单位
BlockingQueue<Runnable> workQueue, // 5. 任务等待队列
ThreadFactory threadFactory, // 6. 线程工厂(强烈建议自定义命名)
RejectedExecutionHandler handler) // 7. 拒绝策略
任务提交的流转顺序(面试高频,也是调参依据):
新任务进来
→ 核心线程未满?创建核心线程执行
→ 核心满了?丢进 workQueue 排队
→ 队列也满了?创建非核心线程(直到 maximumPoolSize)
→ 线程和队列都满了?执行拒绝策略
四种内置拒绝策略:
| 策略 | 行为 |
|---|---|
AbortPolicy(默认) |
直接抛 RejectedExecutionException |
CallerRunsPolicy |
由提交任务的线程自己执行,天然形成"反压" |
DiscardPolicy |
静默丢弃新任务 |
DiscardOldestPolicy |
丢弃队列头部最老的任务,重新提交当前任务 |
生产建议:宁可让调用方感知到异常(Abort)或降速(CallerRuns),也不要无声丢弃。
三、路线一:JDK 原生方式创建线程池
3.1 推荐写法:手动 new ThreadPoolExecutor
java
import java.util.concurrent.*;
public class ThreadPoolDemo {
private static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor(
8, // 核心线程数
16, // 最大线程数
60, TimeUnit.SECONDS, // 非核心线程空闲存活 60 秒
new ArrayBlockingQueue<>(1000), // 有界队列,容量 1000
new NamedThreadFactory("biz-worker"), // 自定义线程工厂
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:调用方执行
);
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
final int taskId = i;
EXECUTOR.execute(() ->
System.out.println(Thread.currentThread().getName() + " 执行任务 " + taskId)
);
}
EXECUTOR.shutdown(); // 记得关闭
}
}
配套的线程工厂(给线程起名字,排查问题时你会感谢自己):
java
public class NamedThreadFactory implements ThreadFactory {
private final AtomicInteger seq = new AtomicInteger(1);
private final String prefix;
public NamedThreadFactory(String prefix) {
this.prefix = prefix;
}
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r, prefix + "-" + seq.getAndIncrement());
t.setDaemon(false);
return t;
}
}
3.2 需要返回值时用 CompletableFuture
execute() 只接收 Runnable。需要拿结果或编排多个异步任务,用 CompletableFuture.supplyAsync:
java
CompletableFuture<String> future = CompletableFuture.supplyAsync(
() -> {
// 模拟耗时调用
sleep(200);
return "查询结果";
},
EXECUTOR // 务必显式传入线程池,别用默认的 ForkJoinPool.commonPool()
);
// 链式编排
future.thenApply(result -> result + " -> 加工后")
.thenAccept(System.out::println)
.exceptionally(ex -> {
log.error("异步任务失败", ex);
return null;
});
⚠️ 不传线程池时,
supplyAsync默认跑在ForkJoinPool.commonPool()上------它是全 JVM 共享的,一旦被慢任务占满,会拖累所有使用者。
3.3 为什么不要用 Executors 工厂方法
Executors 的便捷方法几乎都埋着雷,阿里巴巴《Java 开发手册》明确要求禁用:
| 方法 | 隐患 |
|---|---|
newFixedThreadPool / newSingleThreadExecutor |
队列是无界的 LinkedBlockingQueue,任务堆积可 OOM |
newCachedThreadPool / newScheduledThreadPool |
最大线程数 Integer.MAX_VALUE,可能创建海量线程导致 OOM |
手动 new ThreadPoolExecutor,把队列容量和最大线程数都写成有界的,才是正确姿势。
四、路线二:Spring Boot 3 中配置 ThreadPoolTaskExecutor
在 Spring 项目里更推荐用 ThreadPoolTaskExecutor------它是 ThreadPoolExecutor 的封装,额外提供:
- 与 Spring 容器生命周期集成(优雅关闭);
- 支持
TaskDecorator(透传 MDC、TraceId 等上下文); - 可直接被
@Async、spring.task.execution体系使用。
4.1 声明线程池 Bean
java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
@EnableAsync // 开启 @Async 支持
public class ThreadPoolConfig {
@Bean("bizExecutor")
public Executor bizExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8);
executor.setMaxPoolSize(16);
executor.setQueueCapacity(1000);
executor.setKeepAliveSeconds(60);
executor.setThreadNamePrefix("biz-"); // 线程名前缀
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 优雅关闭:应用停机时等待任务执行完
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor;
}
}
4.2 配合 @Async 使用
java
@Service
public class OrderService {
/** 指定使用名为 bizExecutor 的线程池;不指定则用默认池 */
@Async("bizExecutor")
public CompletableFuture<Void> sendOrderNotice(Long orderId) {
// 发短信、发邮件、推送等耗时旁路逻辑
return CompletableFuture.completedFuture(null);
}
/** 也可以只声明 void,但拿不到完成信号 */
@Async("bizExecutor")
public void auditLog(String action) {
// ...
}
}
@Async 的四个经典失效场景(排查清单):
- 同类内部调用 :
this.sendOrderNotice(...)绕过了代理,异步不生效。解法:注入自身代理、拆到另一个 Bean,或用AopContext.currentProxy(); - 方法不是 public:Spring AOP 代理的默认限制;
- 调用方没有处理返回值异常 :
void异步方法的异常默认被吞掉,需要配置AsyncUncaughtExceptionHandler; - 忘了
@EnableAsync。
异常处理器示例:
java
@Configuration
public class AsyncExceptionHandler implements AsyncConfigurer {
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) ->
log.error("异步方法 {} 执行异常, 参数: {}", method.getName(), Arrays.toString(params), ex);
}
}
4.3 线程上下文透传(TaskDecorator)
异步线程拿不到主线程的 MDC / TraceId,日志链路会断。用 TaskDecorator 包一层:
java
executor.setTaskDecorator(runnable -> {
// 提交时:在调用方线程抓取上下文
Map<String, String> context = MDC.getCopyOfContextMap();
return () -> {
try {
// 执行时:在线程池线程恢复上下文
if (context != null) {
MDC.setContextMap(context);
}
runnable.run();
} finally {
MDC.clear(); // 线程会复用,必须清理
}
};
});
五、路线三:用 Spring Boot 3 配置属性快速配置
不想写配置类,可以直接在 application.yml 里配置 Spring Boot 的默认异步线程池 (TaskExecutionAutoConfiguration 自动装配):
yaml
spring:
task:
execution:
pool:
core-size: 8 # 核心线程数
max-size: 16 # 最大线程数
queue-capacity: 1000 # 队列容量
keep-alive: 60s # 非核心线程空闲存活时间
allow-core-thread-timeout: false
thread-name-prefix: async-
shutdown:
await-termination: true # 优雅关闭
await-termination-period: 30s
配置完成后,不指定池名的 @Async 方法、TaskExecutor 类型的注入点都会使用这个池。
定时任务线程池是另一套独立配置:
spring.task.scheduling.pool.size等,和异步池互不影响。
六、多业务线程池隔离
不同业务共用一个池,慢业务会把快业务饿死。推荐按业务域拆池:
java
@Configuration
public class ThreadPoolConfig {
/** 订单业务池 */
@Bean("orderExecutor")
public Executor orderExecutor() {
return buildExecutor("order-", 8, 16, 1000);
}
/** 报表导出池:重 IO、可排队 */
@Bean("reportExecutor")
public Executor reportExecutor() {
return buildExecutor("report-", 4, 8, 5000);
}
private Executor buildExecutor(String prefix, int core, int max, int queue) {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(core);
executor.setMaxPoolSize(max);
executor.setQueueCapacity(queue);
executor.setThreadNamePrefix(prefix);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setAwaitTerminationSeconds(30);
executor.initialize();
return executor;
}
}
使用时按名注入:
java
@Async("orderExecutor")
public void handleOrder(Long orderId) { ... }
// 或直接注入
@Resource(name = "reportExecutor")
private Executor reportExecutor;
七、参数怎么定:一个实用的起点
没有银弹公式,但可以用经典经验作为初始值,再压测调整:
- CPU 密集型 (计算、编解码):
核心数 ≈ CPU 核数 + 1; - IO 密集型 (HTTP/RPC/DB):
核心数 ≈ CPU 核数 × 2(或按核数 × (1 + 平均等待时间/平均计算时间)估算); - 混合型:拆成两个池分别配置。
比公式更重要的是两件事:
- 队列必须有界,容量按"可接受的最大堆积延迟 × 消费速率"倒推;
- 上线后看监控调参,而不是拍脑袋。
八、线程池监控
线程池出问题往往有前兆:队列越积越长、活跃线程打满。把指标暴露出来:
8.1 简单定时打印
java
@Component
@RequiredArgsConstructor
public class ThreadPoolMonitor {
@Resource(name = "bizExecutor")
private Executor bizExecutor;
@Scheduled(fixedRate = 10_000)
public void report() {
if (bizExecutor instanceof ThreadPoolTaskExecutor taskExecutor) {
ThreadPoolExecutor pool = taskExecutor.getThreadPoolExecutor();
log.info("线程池状态 | 活跃: {} | 池大小: {} | 队列: {} | 已完成: {}",
pool.getActiveCount(),
pool.getPoolSize(),
pool.getQueue().size(),
pool.getCompletedTaskCount());
}
}
}
8.2 接入 Micrometer(Spring Boot Actuator)
java
import io.micrometer.core.instrument.binder.ExecutorServiceMetrics;
@Bean("bizExecutor")
public Executor bizExecutor(MeterRegistry registry) {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// ... 参数配置省略
executor.initialize();
// 把指标绑定到 MeterRegistry,即可通过 /actuator/metrics 查询
ExecutorServiceMetrics.monitor(registry,
executor.getThreadPoolExecutor(), "bizExecutor");
return executor;
}
之后可查询 executor_active_threads、executor_queued_tasks 等指标,配合告警规则使用。
九、优雅关闭
Spring Boot 3 应用收到停机信号(如 K8s 滚动更新)时,如果线程池里还有任务在跑,直接杀进程就会丢任务。两个关键点:
- 线程池侧:
setWaitForTasksToCompleteOnShutdown(true)+setAwaitTerminationSeconds(30)(或 yaml 中spring.task.execution.shutdown.*); - Web 容器侧:开启
server.shutdown=graceful,先停止接收新请求:
yaml
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 40s # 略大于线程池等待时间
十、常见坑汇总
| 坑 | 说明与对策 |
|---|---|
用 Executors.newXxx |
无界队列/无上限线程,OOM 风险;手动构建有界池 |
CompletableFuture 不传线程池 |
默认共用 ForkJoinPool.commonPool();务必显式传池 |
@Async 同类调用失效 |
走代理才生效;拆 Bean 或注入自身代理 |
| 异步异常被吞 | void 方法配 AsyncUncaughtExceptionHandler,或改用 CompletableFuture 返回 |
| 线程无名 | 出问题时线程 dump 全是 pool-1-thread-3;统一设置 threadNamePrefix |
| MDC/TraceId 丢失 | 用 TaskDecorator 透传并在 finally 里清理 |
| 所有业务共用一个池 | 按业务域隔离,慢任务不拖累快任务 |
| 忘了关闭线程池 | 非 Spring 管理的池要 shutdown();Spring 管理的交给容器生命周期 |
十一、写在最后
关于虚拟线程(Virtual Threads)补充一句:它是 JDK 21 正式特性 (JDK 19/20 为预览),JDK 17 还用不上。不过 Spring Boot 3.2+ 已提前提供了开关 spring.threads.virtual.enabled=true,升级到 JDK 21 后可一键让异步任务、Tomcat 请求线程切换到虚拟线程。对于 IO 密集型系统,这是一次值得关注的范式变化------但在 JDK 17 上,把传统线程池配好、监控好,依然是最稳的基本功。
一句话总结:手动构建有界池 + 自定义线程名 + 按业务隔离 + 优雅关闭 + 暴露监控指标,做到这五点,线程池就不会成为系统里的黑盒。
参考:Spring Boot 3.x 官方文档 Task Execution 章节、Doug Lea《Java 并发编程实战》、阿里巴巴《Java 开发手册》。