应用重启时,线程池里的任务怎么办?直接“拔电源”,正在执行的任务会丢失,积压的队列也可能被清空。要做到“优雅关停”,就得在停止服务前,给线程池一个体面的收场:尽力完成进行中的任务、合理地放弃等待,并确保资源回收

本文将讲解线程池关闭的核心机制,并给出标准化的优雅停机范式。


一、线程池的状态:先看懂“门牌号”

线程池内部维护了一个状态机,关闭操作本质上就是改变状态。理解它能帮你预测 API 行为。

状态

描述

RUNNING

正常工作,接受新任务,处理队列中任务

SHUTDOWN

不接收新任务,但会处理完队列中已有的任务

STOP

不接收新任务,也不处理队列中任务,并尝试中断正在执行的任务

TIDYING

所有任务都已终止,工作线程数为0,准备调用 terminated()

TERMINATED

terminated() 执行完毕

核心方法正好对应两个状态转折:

  • shutdown():RUNNING → SHUTDOWN

  • shutdownNow():RUNNING 或 SHUTDOWN → STOP


二、shutdown() vs shutdownNow():两种“关门”哲学

1. shutdown() —— 温柔地“停止接单”

java

ExecutorService pool = Executors.newFixedThreadPool(3);
pool.shutdown();

效果:

  • 线程池状态变为 SHUTDOWN

  • 不能再提交新任务,否则抛出 RejectedExecutionException

  • 正在执行的任务会继续运行至结束

  • 队列中等待的任务会被执行完毕

  • 所有任务完成后,线程池进入 TERMINATED。

这是一种“自然闭店”逻辑:不接新客,但已进店的客人和排队的客人全部服务完。

2. shutdownNow() —— 紧急“清场”

java

List<Runnable> notExecuted = pool.shutdownNow();

效果:

  • 线程池状态变为 STOP

  • 不再接收新任务

  • 正在执行的任务会收到中断信号(调用 Thread.interrupt())。

  • 队列中等待的任务不再执行,直接清空,并将它们作为返回值返回。

  • 试图通过中断让正在运行的任务快速停止。

这相当于“停业疏散”:试图中断店内顾客,排队的人直接请走。

注意: shutdownNow() 能中断线程,但如果任务内部不响应中断(例如没有检查 InterruptedExceptionThread.currentThread().isInterrupted()),任务依然会继续执行,无法达到真正的“立即停止”效果。


三、优雅关停的三阶范式

理想的优雅关停并不是简单的 shutdown()shutdownNow(),而是两者的结合,配合超时控制中断

标准模板

java

public void gracefulShutdown(ExecutorService pool, long timeout, TimeUnit unit) {
    // 第一步:温柔关门,不再接收新任务
    pool.shutdown();
    try {
        // 第二步:等待一段时间,让已提交的任务执行完
        if (!pool.awaitTermination(timeout, unit)) {
            // 第三步:超时后,强制中断所有正在执行的任务
            pool.shutdownNow();
            // 再给一次机会,等待强制中断后任务响应中断
            if (!pool.awaitTermination(timeout, unit)) {
                // 真正超时,记录错误或做降级处理
                log.error("线程池未能在超时时间内完全终止");
            }
        }
    } catch (InterruptedException e) {
        // 当前线程被中断,也尝试强制关闭
        pool.shutdownNow();
        Thread.currentThread().interrupt();
    }
}

关键动作拆解:

  1. shutdown():拒接新任务,然后开始等待。

  2. awaitTermination(timeout):阻塞当前线程,等待线程池达到 TERMINATED 状态(即所有任务执行完毕),最长等待 timeout 时间。返回 true 表示完成,false 表示超时。

  3. shutdownNow():超时还没结束,说明队列可能过长,或任务卡死。此时强行中断并清空队列。

  4. 再次 awaitTermination():给被中断的任务一点时间处理中断逻辑,避免挂死。

  5. 中断处理:当前线程(主线程)若在等待期间被中断,也要保证 shutdownNow() 执行,然后恢复中断状态。

为什么需要这么久?—— “中断”只是信号,不是暴力拔刀

shutdownNow() 调用 Thread.interrupt(),任务如果阻塞在 Object.wait()Thread.sleep()BlockingQueue.take() 等可响应中断的调用上,会立即抛出 InterruptedException 并退出。但如果任务是纯计算密集且不检查中断标志,那可能永远停不下来。因此,编写线程池任务时应遵守规范:

  • 长时间循环中,先检查中断标志:if (Thread.currentThread().isInterrupted()) break;

  • 正确处理 InterruptedException,不要生吞异常,通常需要退出或设置中断标志:Thread.currentThread().interrupt();


四、实际场景:Spring Boot 应用关闭时优雅停线程池

在 Spring Boot 中,你可以借助 DisposableBean@PreDestroy 来触发线程池的优雅关停。

java

@Configuration
public class ThreadPoolConfig {
    @Bean("orderPool")
    public ExecutorService orderPool() {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
            5, 10, 60, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(100),
            new ThreadPoolExecutor.CallerRunsPolicy()
        );
        return pool;
    }

    @Bean
    public ShutdownHook shutdownHook(@Qualifier("orderPool") ExecutorService pool) {
        return new ShutdownHook(pool, 30, TimeUnit.SECONDS);
    }

    static class ShutdownHook implements DisposableBean {
        private final ExecutorService pool;
        private final long timeout;
        private final TimeUnit unit;

        public ShutdownHook(ExecutorService pool, long timeout, TimeUnit unit) {
            this.pool = pool;
            this.timeout = timeout;
            this.unit = unit;
        }

        @Override
        public void destroy() throws Exception {
            // 调用前面定义的优雅停机方法
            gracefulShutdown(pool, timeout, unit);
        }
    }
}

当容器关闭时,Spring 会调用 DisposableBean.destroy(),从而触发线程池的优雅停机。

如果是 Spring Boot 2.3+,还可以配置 server.shutdown=gracefulspring.lifecycle.timeout-per-shutdown-phase 来控制 Web 容器的优雅关闭,但业务自己的线程池建议像上面这样手动管理。


五、shutdownNow() 后未执行的任务,如何跟踪?

shutdownNow() 会返回一个未执行任务列表,你可以记录它们,用于事后补偿或告警。

java

List<Runnable> abandoned = pool.shutdownNow();
if (!abandoned.isEmpty()) {
    log.warn("线程池关闭时有 {} 个任务未被处理", abandoned.size());
    // 可将这些任务持久化到数据库,或放入备用队列稍后重试
}

然而,正在执行的任务是无法通过这种方式获取的。如果需要监控哪些任务被强制停止,必须在任务内部 catch (InterruptedException) 时主动记录日志。


六、常见误区与雷区

  1. 直接调用 shutdownNow() 然后不管 forget
    你丢失了队列中的任务,且如果不等待,甚至不确定线程是否真正结束。

  2. 调了 shutdown() 后还往池里扔任务
    shutdown() 后线程池状态是 SHUTDOWN,提交新任务会触发拒绝策略。如果不配置拒绝策略,默认抛异常中止。务必先关闭再 gracefully wait。

  3. 任务不响应中断
    如果你的 Runnable 永远不会检查中断,shutdownNow() 将永远无法停止它。务必将循环任务加入中断检查。

  4. 忘记 awaitTermination 的时间
    设置超时很重要,避免主线程被意外卡死。根据任务平均处理时间乘以一个系数,再加一点缓冲。


七、总结

概念

含义

要点

shutdown()

温柔停服

等任务做完,队列清空

shutdownNow()

强制中断

试图中断正在执行的任务,返回未执行任务列表

awaitTermination()

阻塞等待终止

设定超时,是优雅关停的关键纽带

优雅关停范式

shutdown → awaitTermination → shutdownNow → awaitTermination

尽全力温柔关闭,超时则强制,再给中断响应一次机会

优雅的根本在于:

  • 给业务一个善终的机会。

  • 给系统一个可控的时间窗口。

  • 给未完成的任务一个清晰的后续处理路径。

将这套模式固化为工具类,在每个线程池关闭时复用,就能彻底告别“杀进程”式的粗暴停服了。