一、问题背景
在 Spring Boot 项目中,我们经常会用到线程池来执行后台任务。但如果线程池没有正确关闭,会导致进程无法退出、K8s 滚动更新超时等问题。
先看一段代码:
java
@Bean(destroyMethod = "shutdown")
public LockManager lockManager() {
return new LockManager();
}
这个 destroyMethod = "shutdown" 到底做了什么?为什么需要它?
二、destroyMethod 的作用
2.1 Spring Bean 的生命周期
启动 → 实例化 → 依赖注入 → 初始化 → 使用 → 销毁
destroyMethod 就是告诉 Spring:容器关闭时,调用这个 Bean 实例上的哪个方法来做资源清理。
2.2 调用链路
Spring 容器关闭
└─ 发现 @Bean(destroyMethod = "shutdown")
└─ 反射调用 instance.shutdown() ← 调用的是实例对象的方法
└─ 方法体内:scheduler.shutdownNow() ← 你自己写的清理逻辑
对应代码:
java
public class LockManager {
private final ScheduledExecutorService scheduler =
Executors.newSingleThreadScheduledExecutor();
public LockManager() {
startCleanupTask(); // 启动后台清理线程
}
// Spring 关闭时调用这个方法
public void shutdown() {
scheduler.shutdownNow(); // 关闭线程池
}
}
2.3 关键区分
| 名字 | 来源 | 作用 |
|---|---|---|
lockManager() |
@Bean 方法名 | Bean 的注册名,工厂方法,只调用一次 |
shutdown |
destroyMethod 属性 | Bean 实例上要调用的销毁方法名 |
Spring 只负责"找到并调用 shutdown()",不关心方法内部做什么。你在 shutdown() 里写什么,它就执行什么。
三、为什么线程池需要手动关闭
3.1 核心原因:非守护线程
Executors 工厂方法创建的线程池,默认都是非守护线程:
java
// Executors.defaultThreadFactory() 源码
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r, namePrefix + threadNumber.getAndIncrement(), 0);
t.setDaemon(false); // ← 写死了非守护
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
| 线程类型 | JVM 行为 |
|---|---|
| 守护线程(daemon=true) | JVM 不等它,直接退出 |
| 非守护线程(daemon=false) | JVM 会等它执行完才退出 ← 默认是这个 |
所有 Executors 工厂方法默认都是非守护线程:
| 方法 | 默认 daemon |
|---|---|
newSingleThreadScheduledExecutor() |
false |
newScheduledThreadPool(n) |
false |
newFixedThreadPool(n) |
false |
newCachedThreadPool() |
false |
newSingleThreadExecutor() |
false |
3.2 不关闭会怎样
情况一:正常关闭(kill -15 / Ctrl+C)
JVM 收到信号
└─ Spring ShutdownHook 执行,容器关闭
└─ 但 scheduler 线程还在跑(非守护线程)
└─ Spring 容器已关闭,但 JVM 还在等这个线程
└─ 结果:进程卡住几秒 → 超时后强制终止
情况二:代码中主动调用 context.close()
main 方法执行完
└─ 但 scheduler 是非守护线程
└─ JVM 不退出,进程挂住,永远不返回
情况三:kill -9
无论配不配 destroyMethod,都一样:
- JVM 直接被杀,什么都不执行
3.3 实际生产影响
| 影响项 | 严重程度 | 说明 |
|---|---|---|
| 进程无法退出 | 高 | 嵌入式/单元测试场景下进程挂死 |
| 关闭变慢 | 中 | 正常 kill 时多卡几秒 |
| 资源泄漏 | 中 | 线程未释放,端口/文件句柄未关闭 |
| K8s 滚动更新 | 高 | 旧 Pod 关不掉 → 超过 terminationGracePeriodSeconds → SIGKILL |
| 任务执行不完整 | 低 | 清理任务可能被中途打断 |
四、项目中的三种关闭方式
4.1 @Bean(destroyMethod = "shutdown")
适用于:自定义 Bean 内部持有线程池
java
@Configuration
public class AppConfig {
@Bean(destroyMethod = "shutdown")
public LockManager lockManager() {
return new LockManager();
}
}
public class LockManager {
private final ScheduledExecutorService scheduler =
Executors.newSingleThreadScheduledExecutor();
public LockManager() {
startCleanupTask();
}
public void shutdown() {
scheduler.shutdownNow();
}
}
4.2 @PreDestroy 注解
适用于:Spring 组件(@Component)内部持有线程池
java
@Component
public class DataToolMethod {
private final ScheduledExecutorService scheduler =
Executors.newSingleThreadScheduledExecutor();
@PostConstruct
public void init() {
scheduler.scheduleAtFixedRate(() -> {
// 定时刷新缓存
}, 1, 1, TimeUnit.MINUTES);
}
@PreDestroy
public void destroy() {
if (scheduler != null && !scheduler.isShutdown()) {
scheduler.shutdown();
}
}
}
4.3 finally 手动关闭
适用于:方法级线程池,用完即关
java
private List<ReturnShoppingCard> add(...) {
ExecutorService executor = null;
try {
executor = Executors.newSingleThreadExecutor();
final Future<?> future = executor.submit(() -> {
// 业务逻辑
});
future.get(4400, TimeUnit.MILLISECONDS);
} catch (Exception e) {
future.cancel(true);
} finally {
executor.shutdown(); // 用完即关
}
return list;
}
4.4 三种方式对比
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| destroyMethod | 自定义 Bean | 配置简单 | 方法名写死在注解里 |
| @PreDestroy | Spring 组件 | 语义清晰 | 需要单独写方法 |
| finally | 方法级线程池 | 即时释放 | 每次创建销毁有开销 |
五、最佳实践
5.1 优先使用 Spring 管理的线程池
Spring 提供了 ThreadPoolTaskExecutor,自带优雅关闭,不需要手动 shutdown():
java
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("taskExecutor")
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("task-");
executor.setWaitForTasksToCompleteOnShutdown(true); // 等待任务完成
executor.setAwaitTerminationSeconds(30); // 最多等30秒
executor.initialize();
return executor;
}
}
5.2 如果用原生 ExecutorService,一定要关闭
java
@Component
public class MyService {
private final ScheduledExecutorService scheduler =
Executors.newSingleThreadScheduledExecutor();
@PreDestroy
public void destroy() {
scheduler.shutdown(); // 停止接收新任务
try {
if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) {
scheduler.shutdownNow(); // 强制停止
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
5.3 方法级线程池不要每次都 new
java
// ❌ 不推荐:每次调用都创建销毁线程池
public void process() {
ExecutorService executor = Executors.newSingleThreadExecutor();
try {
executor.submit(() -> { ... });
} finally {
executor.shutdown();
}
}
// ✅ 推荐:复用类级共享线程池
@Service
public class MyService {
private final ExecutorService executor = Executors.newSingleThreadExecutor();
@PreDestroy
public void destroy() {
executor.shutdown();
}
public void process() {
executor.submit(() -> { ... });
}
}
六、总结
| 问题 | 答案 |
|---|---|
| destroyMethod 调用的是谁? | Bean 实例上的同名方法,不是 @Bean 工厂方法 |
| 为什么需要手动关闭线程池? | Executors 默认创建非守护线程,会阻止 JVM 退出 |
| 不关闭有什么影响? | 进程挂住、K8s 滚动更新超时、资源泄漏 |
| 关闭线程池的三种方式? | destroyMethod、@PreDestroy、finally |
| 最佳实践是什么? | 优先用 Spring 的 ThreadPoolTaskExecutor,自带优雅关闭 |