Spring Boot 线程池关闭:destroyMethod 的作用与最佳实践

📅 2026/7/30 14:51:26 👁️ 阅读次数 📝 编程学习
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 的注册名,工厂方法,只调用一次
shutdowndestroyMethod 属性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配置简单方法名写死在注解里
@PreDestroySpring 组件语义清晰需要单独写方法
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,自带优雅关闭