线程池如何优雅关闭?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() → 记录未执行任务。少任何一步,都会在发布时丢掉正在处理或排队中的请求。