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

一、问题背景

在 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,自带优雅关闭
相关推荐
Maiko Star15 分钟前
* LangChain 提示词模板详解:ChatPromptTemplate 的使用与高级特性
java·人工智能·langchain
波力海苔夹心脆6751 小时前
C# 序列化与反序列化详解:System.Text.Json、Newtonsoft.Json、XmlSerializer 用法、特性选项与安全实践
经验分享·后端·c#·json·.net
现任明教教主~2 小时前
Thinkphp站群蜘蛛池SaaS系统YanyvSEO含多用户/积分/六大引擎计费/易支付对接
java·开发语言·spring
JavaGuide2 小时前
NVIDIA 又开源了!这次给 AI Agent 加上权限管控
前端·后端
excel2 小时前
prisma 如何处理数据库竞态
前端·数据库·后端
一条破秋裤3 小时前
Linux 线程创建:pthread_create 与基本回收
java·linux·运维
布吉岛的石头3 小时前
Java 程序员第 49 阶段5:BERT 预训练目标 MLM+NSP 的工程含义
java·人工智能·深度学习·bert·transformer
摇滚侠4 小时前
《On Java 中文版 基础卷》阅读笔记 对象无处不在 03
java·笔记·python
二十雨辰4 小时前
[学成在线]-01项目背景
java