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,自带优雅关闭
相关推荐
n8n4 小时前
Spring AI 三层架构详解:ChatModel → ChatClient → Controller,同步调用 vs 流式调用(SSE)
后端
用户3126874877204 小时前
线程池到底怎么管理线程的?从 ThreadPoolExecutor 到拒绝策略全链路拆解
java
小傅哥4 小时前
Java + DDD,1:1 复刻 Deepseek Harness 项目
前端·后端·ai编程
Lambert2814 小时前
AgentScope Java 从零(03):Agent 的记性默认全开,我在第 50 轮翻了车
后端·aigc
积硅步致千里5 小时前
Word 里的 .wmf 其实是 EMF:一次 metafile 转 PNG 排障
前端·后端
cpolar技术支持5 小时前
Kafka Streams 窗口统计怎么验收:本地跑订单流聚合,用 cpolar 给同事看只读结果页
java·docker·kafka·cpolar·kafka streams
爱读源码的大都督5 小时前
DeepSeek面试官问:生产RAG系统回答不准确,该如何定位和优化?这样回答,能让面试官当场给你Offer!
java·后端·python
杨运交5 小时前
[069][公共模块]Spring Boot 全局异常处理与参数校验实战(下):校验异常精细化处理与 WebFlux 适配
java·spring boot·后端
行者-全栈开发5 小时前
Spring Boot + FFmpeg 视频批量处理实战:压缩、HLS切片与异步任务引擎
spring boot·ffmpeg·异步处理·视频压缩·hls切片·批量任务·redis队列
Raas1006 小时前
MAI Gateway(魔芋企业级AI网关)对比分析:AI网关和OpenRouter区别?企业级能力差距一览
java·服务器·网络·人工智能·gateway·ai网关·mai gateway