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,自带优雅关闭
相关推荐
古法老登瞎折腾6 分钟前
大数据智能运维实战(三):实现第一个运维 MCP 工具——查询节点与组件状态
java·大数据·运维·人工智能·spring ai·mcp
小蒜学长8 分钟前
基于数据挖掘的京东商城商品推荐系统设计与实现(代码+数据库+LW)
java·数据库·spring boot·后端·推荐系统·个性化推荐
IT_陈寒17 分钟前
Vue开发实战:从入门到精通
前端·人工智能·后端
IT研究室22 分钟前
最新计算机毕业设计选题推荐-基于spring boot的医院体检管理系统-网站-文档指导-Java-springboot
java·spring boot·课程设计
长不大的杨瑞杰26 分钟前
把 Spring Boot 接口变成 AI 工具:OpenAPI、参数映射与异常语义化实战
人工智能·spring boot·后端
有同事要进步27 分钟前
Leecode第68题:x的平方根
java·数据结构·算法
摇滚侠29 分钟前
使用 VSCode 开发 Java 经验分享
java·经验分享·vscode
张彦峰ZYF30 分钟前
一条消息的丢与重:Kafka 与 RabbitMQ 可靠投递的确认、位点、死信与幂等全链路
后端·kafka·消息队列·rabbitmq·isr·解耦与削峰·丢失与重复
吹什么轩33 分钟前
linux系统复习:进程优先级与O(1)调度
java·linux·算法
RuoyiOffice34 分钟前
Vue3+TypeScript 企业管理平台统一UI:新增一个模块,为什么还要重新调样式?
spring boot·typescript·vue3·组件化开发·企业管理软件·ruoyi office·统一ui