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

一、问题背景

在 Spring Boot 项目中,我们经常会用到线程池来执行后台任务。但如果线程池没有正确关闭,会导致进程无法退出、K8s 滚动更新超时等问题。

先看一段代码:

@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()  ← 你自己写的清理逻辑

对应代码:

public class LockManager {
    private final ScheduledExecutorService scheduler = 
        Executors.newSingleThreadScheduledExecutor();

    public LockManager() {
        startCleanupTask();  // 启动后台清理线程
    }

    // Spring 关闭时调用这个方法
    public void shutdown() {
        scheduler.shutdownNow();  // 关闭线程池
    }
}

2.3 关键区分

名字来源作用
lockManager()@Bean 方法名Bean 的注册名,工厂方法,只调用一次
shutdowndestroyMethod 属性Bean 实例上要调用的销毁方法名

Spring 只负责"找到并调用 shutdown()",不关心方法内部做什么。你在 shutdown() 里写什么,它就执行什么。

三、为什么线程池需要手动关闭

3.1 核心原因:非守护线程

Executors 工厂方法创建的线程池,默认都是非守护线程:

// 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 内部持有线程池

@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)内部持有线程池

@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 手动关闭

适用于:方法级线程池,用完即关

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配置简单方法名写死在注解里
@PreDestroySpring 组件语义清晰需要单独写方法
finally方法级线程池即时释放每次创建销毁有开销

五、最佳实践

5.1 优先使用 Spring 管理的线程池

Spring 提供了 ThreadPoolTaskExecutor,自带优雅关闭,不需要手动 shutdown()

@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,一定要关闭

@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

// ❌ 不推荐:每次调用都创建销毁线程池
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,自带优雅关闭
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT_Octopus

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值