Spring Boot 线程池关闭:destroyMethod 的作用与最佳实践
一、问题背景
在 Spring Boot 项目中,我们经常会用到线程池来执行后台任务。但如果线程池没有正确关闭,会导致进程无法退出、K8s 滚动更新超时等问题。
先看一段代码:
@Bean(destroyMethod="shutdown")publicLockManagerlockManager(){returnnewLockManager();}这个destroyMethod = "shutdown"到底做了什么?为什么需要它?
二、destroyMethod 的作用
2.1 Spring Bean 的生命周期
启动 → 实例化 → 依赖注入 → 初始化 → 使用 → 销毁destroyMethod就是告诉 Spring:容器关闭时,调用这个 Bean 实例上的哪个方法来做资源清理。
2.2 调用链路
Spring 容器关闭 └─ 发现 @Bean(destroyMethod = "shutdown") └─ 反射调用 instance.shutdown() ← 调用的是实例对象的方法 └─ 方法体内:scheduler.shutdownNow() ← 你自己写的清理逻辑对应代码:
publicclassLockManager{privatefinalScheduledExecutorServicescheduler=Executors.newSingleThreadScheduledExecutor();publicLockManager(){startCleanupTask();// 启动后台清理线程}// Spring 关闭时调用这个方法publicvoidshutdown(){scheduler.shutdownNow();// 关闭线程池}}2.3 关键区分
| 名字 | 来源 | 作用 |
|---|---|---|
lockManager() | @Bean 方法名 | Bean 的注册名,工厂方法,只调用一次 |
shutdown | destroyMethod 属性 | Bean 实例上要调用的销毁方法名 |
Spring 只负责"找到并调用shutdown()",不关心方法内部做什么。你在shutdown()里写什么,它就执行什么。
三、为什么线程池需要手动关闭
3.1 核心原因:非守护线程
Executors工厂方法创建的线程池,默认都是非守护线程:
// Executors.defaultThreadFactory() 源码publicThreadnewThread(Runnabler){Threadt=newThread(group,r,namePrefix+threadNumber.getAndIncrement(),0);t.setDaemon(false);// ← 写死了非守护t.setPriority(Thread.NORM_PRIORITY);returnt;}| 线程类型 | 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 内部持有线程池
@ConfigurationpublicclassAppConfig{@Bean(destroyMethod="shutdown")publicLockManagerlockManager(){returnnewLockManager();}}publicclassLockManager{privatefinalScheduledExecutorServicescheduler=Executors.newSingleThreadScheduledExecutor();publicLockManager(){startCleanupTask();}publicvoidshutdown(){scheduler.shutdownNow();}}4.2 @PreDestroy 注解
适用于:Spring 组件(@Component)内部持有线程池
@ComponentpublicclassDataToolMethod{privatefinalScheduledExecutorServicescheduler=Executors.newSingleThreadScheduledExecutor();@PostConstructpublicvoidinit(){scheduler.scheduleAtFixedRate(()->{// 定时刷新缓存},1,1,TimeUnit.MINUTES);}@PreDestroypublicvoiddestroy(){if(scheduler!=null&&!scheduler.isShutdown()){scheduler.shutdown();}}}4.3 finally 手动关闭
适用于:方法级线程池,用完即关
privateList<ReturnShoppingCard>add(...){ExecutorServiceexecutor=null;try{executor=Executors.newSingleThreadExecutor();finalFuture<?>future=executor.submit(()->{// 业务逻辑});future.get(4400,TimeUnit.MILLISECONDS);}catch(Exceptione){future.cancel(true);}finally{executor.shutdown();// 用完即关}returnlist;}4.4 三种方式对比
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| destroyMethod | 自定义 Bean | 配置简单 | 方法名写死在注解里 |
| @PreDestroy | Spring 组件 | 语义清晰 | 需要单独写方法 |
| finally | 方法级线程池 | 即时释放 | 每次创建销毁有开销 |
五、最佳实践
5.1 优先使用 Spring 管理的线程池
Spring 提供了ThreadPoolTaskExecutor,自带优雅关闭,不需要手动shutdown():
@Configuration@EnableAsyncpublicclassAsyncConfig{@Bean("taskExecutor")publicThreadPoolTaskExecutortaskExecutor(){ThreadPoolTaskExecutorexecutor=newThreadPoolTaskExecutor();executor.setCorePoolSize(4);executor.setMaxPoolSize(8);executor.setQueueCapacity(100);executor.setThreadNamePrefix("task-");executor.setWaitForTasksToCompleteOnShutdown(true);// 等待任务完成executor.setAwaitTerminationSeconds(30);// 最多等30秒executor.initialize();returnexecutor;}}5.2 如果用原生 ExecutorService,一定要关闭
@ComponentpublicclassMyService{privatefinalScheduledExecutorServicescheduler=Executors.newSingleThreadScheduledExecutor();@PreDestroypublicvoiddestroy(){scheduler.shutdown();// 停止接收新任务try{if(!scheduler.awaitTermination(10,TimeUnit.SECONDS)){scheduler.shutdownNow();// 强制停止}}catch(InterruptedExceptione){scheduler.shutdownNow();Thread.currentThread().interrupt();}}}5.3 方法级线程池不要每次都 new
// ❌ 不推荐:每次调用都创建销毁线程池publicvoidprocess(){ExecutorServiceexecutor=Executors.newSingleThreadExecutor();try{executor.submit(()->{...});}finally{executor.shutdown();}}// ✅ 推荐:复用类级共享线程池@ServicepublicclassMyService{privatefinalExecutorServiceexecutor=Executors.newSingleThreadExecutor();@PreDestroypublicvoiddestroy(){executor.shutdown();}publicvoidprocess(){executor.submit(()->{...});}}六、总结
| 问题 | 答案 |
|---|---|
| destroyMethod 调用的是谁? | Bean 实例上的同名方法,不是 @Bean 工厂方法 |
| 为什么需要手动关闭线程池? | Executors 默认创建非守护线程,会阻止 JVM 退出 |
| 不关闭有什么影响? | 进程挂住、K8s 滚动更新超时、资源泄漏 |
| 关闭线程池的三种方式? | destroyMethod、@PreDestroy、finally |
| 最佳实践是什么? | 优先用 Spring 的 ThreadPoolTaskExecutor,自带优雅关闭 |
