在 Java 中使用线程池时,ThreadPoolExecutor 背后维护了一个完整的生命周期状态模型。理解这些状态,有助于我们更好地调试线程池异常、避免任务丢失、掌握线程池终止机制。
一、线程池的 5 种核心状态
线程池状态通过一个高位字段控制,低位用于表示 worker 数量,状态定义如下:
状态名 | 含义描述 |
---|---|
RUNNING | 接受新任务 + 处理队列中的任务 |
SHUTDOWN | 不再接受新任务,但继续处理队列任务 |
STOP | 不接受新任务,且不处理队列任务,中断正在运行任务 |
TIDYING | 所有任务执行完毕,worker 线程已清空,进入清理阶段 |
TERMINATED | 清理完毕,线程池彻底终止 |
二、源码实现:状态是如何编码的?
线程池使用一个 ctl 字段管理状态和线程数:
// 高 3 位表示线程池状态,低 29 位表示工作线程数
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 状态位编码
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;
这是一个典型的位运算优化模型:将状态和线程数压缩在一个 int 中,支持原子操作、提高性能。
三、状态转换流程图(简要版)
四、常见状态转换时机
1. RUNNING → SHUTDOWN
调用 shutdown(),停止接收新任务,但执行已有任务。
executor.shutdown();
2. RUNNING → STOP
调用 shutdownNow(),立即中断所有正在执行的任务,清空队列。
executor.shutdownNow();
3. SHUTDOWN / STOP → TIDYING
所有任务完成,线程池空闲,准备回收资源。
4. TIDYING → TERMINATED
执行 terminated() 钩子方法后,彻底终止。
五、注意:状态转换是单向不可逆的
一旦进入 SHUTDOWN 或 STOP,无法回到 RUNNING。这是线程池设计的一种保护机制,避免状态回滚引发不可控行为。
六、实战建议与排查技巧
✅ 如何判断线程池是否已终止?
executor.isTerminated();
✅ 如何优雅关闭线程池?
executor.shutdown();
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
Thread.currentThread().interrupt();
}
✅ 如何扩展线程池终止逻辑?
重写 ThreadPoolExecutor#terminated():
@Override
protected void terminated() {
log.info("线程池已完全终止,执行清理工作...");
}
七、线程池状态与任务丢失的关系
若线程池状态切换到 STOP,队列任务会被清空,未处理任务将直接丢弃,除非你手动处理。
建议在任务提交失败时使用自定义 RejectedExecutionHandler:
new RejectedExecutionHandler() {
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 缓存到 MQ 或数据库中,稍后重试
}
}
八、线程池关闭流程的三个阶段
线程池的关闭是一个多阶段的过程,重点在于:
-
拒绝新任务提交
-
等待已提交任务执行完成
-
清理工作线程和资源
对应的调用顺序如下:
executor.shutdown(); // 拒绝新任务
if (!executor.awaitTermination(timeout, unit)) {
executor.shutdownNow(); // 强制中断
}
这一流程建议在实际项目中封装为工具方法,防止遗漏。
九、两种关闭方式的区别
方法 | 是否接收新任务 | 是否执行队列任务 | 是否中断正在运行的任务 |
---|---|---|---|
shutdown() | ❌ | ✅ | ❌ |
shutdownNow() | ❌ | ❌(清空队列) | ✅(强制中断) |
你可以通过线程池状态 isShutdown() 和 isTerminated() 来判断状态:
if (executor.isShutdown() && executor.isTerminated()) {
log.info("线程池已彻底关闭");
}
十、钩子函数 terminated() 的扩展点
当线程池状态变为 TERMINATED 时,会自动回调 terminated() 方法:
@Override
protected void terminated() {
log.info("自定义清理逻辑:线程池彻底终止");
// 可以发送告警、释放资源、清空队列等
}
该钩子非常适合做资源回收、监控回传、失败任务补偿等操作。
十一、JVM ShutdownHook 与线程池协作
如果线程池在 JVM 关闭时未优雅停止,将导致部分任务丢失或中断。
你可以注册 JVM 级钩子函数,在进程退出前尝试优雅关闭线程池:
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
log.info("JVM 即将退出,尝试关闭线程池...");
executor.shutdown();
try {
if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
executor.shutdownNow();
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
}));
建议每一个线程池都注册该钩子。
十二、常见线程池关闭 Bug 与排查技巧
❌ 线程池未关闭,JVM 无法退出
可能是主线程执行完毕后,线程池中的非守护线程还在运行。
❌ 调用 shutdownNow 后任务丢失
建议设置 RejectedExecutionHandler 缓存未执行任务:
executor.setRejectedExecutionHandler((r, exec) -> {
backupQueue.offer(r); // 放入备用队列或数据库
});
十三、监控建议:状态 + 任务 + 拒绝指标
建议定期采集以下线程池指标:
-
getActiveCount():当前活跃线程数
-
getQueue().size():队列长度
-
getTaskCount() / getCompletedTaskCount():提交与完成任务数
-
自定义拒绝任务计数器
可以通过 Micrometer、Prometheus 等监控组件集成这些指标,实时反映线程池运行情况。
十四、延伸:如何判断线程池“卡死”?
可参考以下策略:
-
任务长时间未完成:超过 SLA 的时间
-
线程池活跃数 = 最大线程数 且 队列未增长:可能任务阻塞
-
队列堆积但无任务完成:可能死锁或 IO 卡顿
示例判断逻辑:
if (executor.getActiveCount() == executor.getMaximumPoolSize()
&& executor.getQueue().size() > threshold
&& taskCompletedCount未变化) {
// 报警或自动扩容
}
十五、最佳实践总结
-
优先使用 shutdown(),结合 awaitTermination()。
-
注册 ShutdownHook,保障线程池在进程退出前完成清理。
-
实现 terminated() 钩子,做任务回传、统计或告警。
-
定期采集监控指标,发现阻塞或异常行为。
-
对拒绝任务做兜底处理,防止数据丢失。
结语
一个线程池的“善终”,考验的是系统的稳定性与细节掌控力。