线程池如何优雅关闭?shutdown 与 shutdownNow 有什么区别?

#Java#面试八股

线程池如何优雅关闭?shutdown 与 shutdownNow 有什么区别?

结论先行:shutdown() 是"不再接新活,把手上的活干完";shutdownNow() 是"不再接新活,尽量把正在干的和排队的都掐掉"。两者都不阻塞,要等真正结束必须配合 awaitTermination()。优雅关闭的标准姿势是:先停流量 → shutdown → 等一段时间 → 超时再 shutdownNow。

1. 三个方法的分工

方法是否接收新任务已提交任务是否中断线程是否阻塞调用方
shutdown()否继续执行完否否
shutdownNow()否尝试停止,队列任务被取出返回是(interrupt)否
awaitTermination(t, unit)—等待结束否是(最多等 t)
isShutdown()———否
isTerminated()———否

2. 生命周期状态

RUNNING ──shutdown()/shutdownNow()──▶ SHUTDOWN
                                        │ 队列空且所有 worker 退出
                                        ▼
                                     TIDYING ──terminated()──▶ TERMINATED

注意:调了 shutdown() 之后线程池不会立刻停止,它要等队列清空、worker 全部退出才进入 TERMINATED。这期间再提交任务会抛 RejectedExecutionException。

3. 优雅关闭模板

public class GracefulShutdown {
    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                8, 16, 60, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(1000),
                r -> new Thread(r, "biz-pool"));

        // 1. 先摘掉流量入口(服务发现下线、停止接收请求)
        //    这一步不在代码里,但顺序不能错:先停流量,再关池

        // 2. 不再接收新任务,已提交的继续执行
        pool.shutdown();

        // 3. 给一个合理的等待窗口(小于 K8s 的 terminationGracePeriod)
        if (!pool.awaitTermination(30, TimeUnit.SECONDS)) {
            // 4. 超时:返回未执行的任务,并中断正在执行的线程
            List<Runnable> pending = pool.shutdownNow();
            log.warn("强制关闭,未执行任务数={}", pending.size());
            // 5. 再等一小会儿,让响应中断的任务退出
            if (!pool.awaitTermination(5, TimeUnit.SECONDS)) {
                log.error("仍有线程未退出,可能存在不响应中断的任务");
            }
        }
    }
}

Spring 里更省事:

@Bean
public ThreadPoolTaskExecutor bizExecutor() {
    ThreadPoolTaskExecutor ex = new ThreadPoolTaskExecutor();
    ex.setCorePoolSize(8);
    ex.setMaxPoolSize(16);
    ex.setQueueCapacity(500);
    ex.setWaitForTasksToCompleteOnShutdown(true);   // 等价于 shutdown + 等待
    ex.setAwaitTerminationSeconds(30);              // 最多等 30 秒
    ex.initialize();
    return ex;
}

4. 三个容易踩的坑

坑现象正确做法
只调 shutdown() 不 awaitTermination主线程已退出,任务被 JVM 一起带走必须等待,或用守护线程池兜底
先 shutdownNow() 再处理队列里的任务被丢弃且无人知晓先 shutdown,超时后才 shutdownNow 并记录返回的任务
任务不响应中断shutdownNow 后线程仍不退出任务内检查中断标志、阻塞调用设超时

shutdownNow() 返回的 List<Runnable> 是从队列里取出的、从未开始执行的任务,它是"丢失任务"的唯一线索,生产上应该记录或落库补偿。

5. 常见追问

追问一:shutdown() 之后还能提交任务吗? 不能,会抛 RejectedExecutionException。这也是为什么"优雅关闭"必须配合上游摘流量:如果还有请求打进来,用户会直接看到 500。

追问二:shutdownNow() 能保证任务立即停止吗? 不能。它只是调用 interrupt(),任务是否退出取决于自身是否响应中断;不响应中断的任务会一直跑到结束。

追问三:为什么线上经常出现"线程池没关,进程无法退出"? 因为线程池的线程默认是用户线程,会阻止 JVM 退出。shutdown() 只对已被管理的池生效,如果某处 new ThreadPoolExecutor 后从未关闭,进程就一直挂着。可以在 ThreadFactory 里 setDaemon(true) 兜底,但更该做的是统一管理生命周期。

追问四:K8s 里怎么设等待时间? awaitTermination 的时间必须小于 terminationGracePeriodSeconds,否则进程会被 SIGKILL 直接杀掉,等于没优雅。常见配置是 grace 30s、等待 25s。

小结

优雅关闭的完整链条是:摘流量 → shutdown() → awaitTermination → 超时 shutdownNow() → 记录未执行任务。少任何一步,都会在发布时丢掉正在处理或排队中的请求。

笔记加载中…