聊聊线程池中哪几种状态,分别表示什么?8 年 Java 开发:从业务踩坑到源码拆解(附监控实战)
作为一名摸爬滚打 8 年的 Java 开发,踩过最多的线程池坑,几乎都和 "状态" 有关:
-
服务优雅停机时,调用
shutdown()
后还在提交任务,结果被拒绝; -
线程池明明有空闲线程,却不处理队列任务,查了半天才发现是处于
STOP
状态; -
监控面板上 "线程池活跃数" 一直不变,最后定位到线程池已悄悄进入
TERMINATED
状态。
其实线程池的所有行为(接受任务、执行任务、关闭)都和 "状态" 强绑定 ------ 理解了这 5 种状态,才算真的懂线程池。今天不聊空洞的理论,只从业务场景 + 源码解析 + 实战监控出发,带你吃透线程池状态的来龙去脉。
一、先抛业务痛点:为什么要关注线程池状态?
新手常觉得 "线程池状态是底层细节,不用管",但八年开发的经验是:90% 的线程池问题,根源都是对状态理解不清。比如这 3 个典型场景:
-
场景 1:服务停机时任务丢失
线上服务重启前,没等线程池处理完队列任务就强制 kill 进程 ------ 其实如果知道
SHUTDOWN
状态会 "处理完队列任务再关闭",调用shutdown()
后等isTerminated()
返回true
再停机,就能避免任务丢失。 -
场景 2:任务提交被拒绝却找不到原因
明明线程池最大线程数没满、队列也没满,提交任务却抛
RejectedExecutionException
------ 查源码才发现,线程池之前被调用过shutdownNow()
,已进入STOP
状态,拒绝接收任何新任务。 -
场景 3:线程池 "假死"
监控看到线程池有 10 个核心线程,但活跃数一直是 0,队列里堆了几百个任务 ------ 原来线程池处于
TIDYING
状态(所有任务执行完、工作线程清空),只差最后一步进入TERMINATED
,但因为没处理好钩子函数,卡在了中间。
这些问题的本质,都是没搞懂 "线程池在什么状态下能做什么、不能做什么"。所以先记住:线程池的状态决定了它的行为边界,理解状态是用好线程池的前提。
二、线程池的 5 种核心状态:定义 + 业务场景 + 源码拆解
Java 线程池(ThreadPoolExecutor
)的状态存在ctl
变量中(一个 32 位整数,高 3 位存状态,低 29 位存工作线程数),核心有 5 种状态,状态之间的流转是单向的(不能从TERMINATED
回退到RUNNING
)。
先看一张 "状态全景表",后面逐个拆解:
状态名称 | 状态值(十进制) | 核心定义 | 业务场景示例 | 允许的操作(接收新任务 / 执行队列任务) |
---|---|---|---|---|
RUNNING | 119304640 | 正常运行状态:能接收新任务,也能执行队列中积压的任务 | 电商大促时,线程池处理下单后的异步通知任务 | 接收新任务 ✅;执行队列任务 ✅ |
SHUTDOWN | 0 | 平缓关闭状态:不接收新任务,但会执行完队列中已有的任务 | 服务优雅停机时,调用shutdown() ,让线程池处理完队列里的任务再退出 |
接收新任务 ❌;执行队列任务 ✅ |
STOP | 536870912 | 强制关闭状态:不接收新任务,不执行队列任务,且会中断正在执行的任务 | 服务紧急停机(比如发现 bug 需立刻重启),调用shutdownNow() 强制停止所有任务 |
接收新任务 ❌;执行队列任务 ❌ |
TIDYING | 1073741824 | 整理状态:所有任务执行完、工作线程数为 0,准备进入终止状态 | 线程池SHUTDOWN 后,最后一个任务执行完、最后一个工作线程退出时 |
无(过渡状态) |
TERMINATED | 1610612736 | 终止状态:线程池完全关闭,所有资源已释放 | 线程池TIDYING 状态完成后,执行完terminated() 钩子函数 |
无(已关闭) |
1. 状态 1:RUNNING(运行态)------ 线程池的 "正常工作模式"
核心逻辑
-
进入时机 :线程池创建后默认就是
RUNNING
状态,不需要手动触发。 -
源码证据 :
ThreadPoolExecutor
的构造方法中,ctl
初始化为ctlOf(RUNNING, 0)
------ 即 "状态为 RUNNING,工作线程数为 0"。java// ThreadPoolExecutor源码片段 private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); private static final int RUNNING = -1 << COUNT_BITS; // COUNT_BITS是29,所以RUNNING=-2^29=119304640(十进制) private static final int COUNT_BITS = 29; private static int ctlOf(int rs, int wc) { return rs | wc; } // 状态(高3位)和线程数(低29位)合并
业务场景
这是线程池最常用的状态,比如:
- 订单服务的线程池,实时处理 "下单后发送短信" 的任务;
- 数据同步服务的线程池,批量处理 "数据库数据同步到 ES" 的任务。
关键行为
- 能调用
execute()
/submit()
提交新任务; - 工作线程会主动从任务队列中取任务执行;
- 除非调用
shutdown()
或shutdownNow()
,否则会一直保持RUNNING
状态。
2. 状态 2:SHUTDOWN(平缓关闭态)------"先收尾,再下班"
核心逻辑
-
进入时机 :调用
threadPool.shutdown()
方法后,线程池从RUNNING
→SHUTDOWN
。 -
源码关键逻辑 :
shutdown()
方法会先修改状态为SHUTDOWN
,再中断所有空闲的工作线程(正在执行任务的线程不中断,让它们跑完)。scss// ThreadPoolExecutor.shutdown()源码片段 public void shutdown() { final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { checkShutdownAccess(); // 1. 修改线程池状态为SHUTDOWN advanceRunState(SHUTDOWN); // 2. 中断所有空闲的工作线程(正在执行任务的线程不会被中断) interruptIdleWorkers(); onShutdown(); // 钩子函数,可自定义扩展 } finally { mainLock.unlock(); } tryTerminate(); // 尝试进入TIDYING状态 }
业务场景
最典型的是服务优雅停机 :
比如 Spring Boot 服务收到kill -15
信号时,通过@PreDestroy
注解调用线程池的shutdown()
,让线程池处理完队列中已有的任务(比如还没发的短信、没同步的数据),再退出服务,避免任务丢失。
关键行为(新手最容易踩坑)
- ❌ 不能提交新任务 :调用
execute()
会抛RejectedExecutionException
; - ✅ 会执行队列中的存量任务 :即使状态是
SHUTDOWN
,已在队列中的任务会被工作线程执行完; - 如何判断是否处理完?调用
isTerminated()
,返回true
表示所有任务执行完、线程池已关闭。
3. 状态 3:STOP(强制关闭态)------"立刻停手,不管收尾"
核心逻辑
-
进入时机 :调用
threadPool.shutdownNow()
方法后,线程池从RUNNING
→STOP
(如果是SHUTDOWN
状态也会→STOP
)。 -
源码关键逻辑 :
shutdownNow()
会修改状态为STOP
,中断所有工作线程(不管是否在执行任务),并清空任务队列。scss// ThreadPoolExecutor.shutdownNow()源码片段 public List<Runnable> shutdownNow() { List<Runnable> tasks; final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { checkShutdownAccess(); // 1. 修改线程池状态为STOP advanceRunState(STOP); // 2. 中断所有工作线程(包括正在执行任务的线程) interruptWorkers(); // 3. 清空任务队列,返回未执行的任务 tasks = drainQueue(); } finally { mainLock.unlock(); } tryTerminate(); // 尝试进入TIDYING状态 return tasks; }
业务场景
适合紧急停机的场景:
- 线上发现线程池任务有 bug(比如会导致数据错乱),需要立刻停止所有任务;
- 服务监控到线程池卡死(比如所有线程都阻塞在第三方接口调用),调用
shutdownNow()
强制恢复。
关键行为(和 SHUTDOWN 的核心区别)
- ❌ 不接收新任务(和 SHUTDOWN 一致);
- ❌ 不执行队列中的存量任务(直接清空队列,返回未执行的任务列表);
- ❌ 中断正在执行的任务 :通过
Thread.interrupt()
中断工作线程,所以如果任务中没有处理InterruptedException
,可能会导致任务 "伪中断"(比如线程还在执行 IO 操作,中断信号无效)。
4. 状态 4:TIDYING(整理态)------"过渡状态,承上启下"
核心逻辑
-
进入时机 :线程池满足 "任务全处理完 + 工作线程数为 0" 时,会从
SHUTDOWN
或STOP
→TIDYING
。- 从
SHUTDOWN
来:队列任务执行完 + 工作线程数为 0; - 从
STOP
来:队列已清空 + 工作线程数为 0。
- 从
-
源码触发点 :
tryTerminate()
方法会检查是否满足 TIDYING 条件,如果满足则修改状态,并调用terminated()
钩子函数。scss// ThreadPoolExecutor.tryTerminate()源码片段 private void tryTerminate() { for (;;) { int c = ctl.get(); // 满足以下条件之一,不进入TIDYING: // 1. 状态是RUNNING;2. 状态是TIDYING/TERMINATED;3. SHUTDOWN状态但队列非空 if (isRunning(c) || runStateAtLeast(c, TIDYING) || (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty())) return; // 如果工作线程数不为0,中断一个空闲线程(触发线程退出) if (workerCountOf(c) != 0) { interruptIdleWorkers(ONLY_ONE); return; } final ReentrantLock mainLock = this.mainLock; mainLock.lock(); try { // 修改状态为TIDYING if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) { try { terminated(); // 钩子函数,子类可重写(比如释放资源) } finally { // 修改状态为TERMINATED ctl.set(ctlOf(TERMINATED, 0)); termination.signalAll(); // 唤醒等待线程池终止的线程 } return; } } finally { mainLock.unlock(); } } }
业务场景
TIDYING 是过渡状态,业务中很少直接感知到,但有个实用场景:
自定义线程池时,重写terminated()
方法做 "收尾工作",比如打印线程池关闭日志、释放自定义资源(如数据库连接池)。
scala
// 自定义线程池,重写terminated()
class CustomThreadPool extends ThreadPoolExecutor {
public CustomThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
// 线程池进入TERMINATED前触发
@Override
protected void terminated() {
System.out.println("线程池已关闭,释放资源完成!");
// 比如关闭自定义的日志器、数据库连接池等
}
}
关键行为
- TIDYING 是 "瞬时状态",很快会进入 TERMINATED;
- 此时线程池已不处理任何任务,工作线程数为 0;
- 唯一的动作是执行
terminated()
钩子函数。
5. 状态 5:TERMINATED(终止态)------"线程池的终点"
核心逻辑
- 进入时机 :线程池在
TIDYING
状态下执行完terminated()
钩子函数后,会自动进入TERMINATED
状态。 - 源码证据 :上面
tryTerminate()
源码中,terminated()
执行完后,会调用ctl.set(ctlOf(TERMINATED, 0))
,将状态设为 TERMINATED。
业务场景
线程池完全关闭后的最终状态,比如:
- 服务停机流程中,线程池
shutdown()
后,所有任务执行完、terminated()
执行完,进入 TERMINATED; - 紧急停机时,
shutdownNow()
清空队列、中断线程,最后进入 TERMINATED。
关键行为
- 线程池已完全释放所有资源(工作线程、任务队列、锁等);
- 无法再提交任务,也无法恢复到其他状态;
- 调用
isTerminated()
会返回true
,可用于判断线程池是否真的关闭。
三、状态流转图:理清 "从生到死" 的路径
线程池的状态流转是单向的,没有回头路,记住这 4 条核心路径即可:
-
正常关闭路径 :
RUNNING
→SHUTDOWN
→TIDYING
→TERMINATED
- 触发条件:调用
shutdown()
,且队列任务执行完、工作线程清空。 - 业务场景:服务优雅停机。
- 触发条件:调用
-
紧急关闭路径 :
RUNNING
→STOP
→TIDYING
→TERMINATED
- 触发条件:调用
shutdownNow()
,清空队列、中断所有线程,工作线程清空。 - 业务场景:服务紧急重启、任务有 bug 需立刻停止。
- 触发条件:调用
-
特殊路径 1 :
SHUTDOWN
→STOP
→TIDYING
→TERMINATED
- 触发条件:调用
shutdown()
后(已进入 SHUTDOWN),又调用shutdownNow()
。 - 业务场景:优雅停机过程中,发现必须立刻停止(比如超时)。
- 触发条件:调用
-
特殊路径 2 :
RUNNING
→SHUTDOWN
→STOP
→TIDYING
→TERMINATED
- 触发条件:调用
shutdown()
后,队列还没处理完,又调用shutdownNow()
。 - 业务场景:优雅停机超时,强制中断剩余任务。
- 触发条件:调用
四、实战:如何监控和判断线程池状态?(附代码)
理解状态的最终目的是 "解决业务问题",比如监控线程池健康度、排查任务被拒绝的原因。这里分享两种实战方案:
1. 基础方案:用线程池自带方法获取状态
ThreadPoolExecutor
提供了多个方法判断状态,直接用即可:
方法名 | 作用 | 示例(线程池处于 SHUTDOWN 时) |
---|---|---|
isRunning() |
判断是否处于 RUNNING 状态 | 返回 false |
isShutdown() |
判断是否处于 SHUTDOWN/STOP/TIDYING/TERMINATED | 返回 true |
isTerminating() |
判断是否处于 TIDYING 状态 | 返回 false |
isTerminated() |
判断是否处于 TERMINATED 状态 | 返回 false |
业务代码示例:服务停机时等待线程池关闭
kotlin
// Spring Boot服务优雅停机示例(@PreDestroy)
@Component
public class ThreadPoolShutdownConfig {
@Autowired
private ThreadPoolExecutor orderThreadPool; // 自定义的订单线程池
@PreDestroy
public void shutdownThreadPool() {
System.out.println("开始关闭订单线程池...");
// 1. 调用shutdown(),进入SHUTDOWN状态
orderThreadPool.shutdown();
try {
// 2. 等待60秒,直到线程池进入TERMINATED状态
if (!orderThreadPool.awaitTermination(60, TimeUnit.SECONDS)) {
// 3. 超时未关闭,调用shutdownNow()强制关闭
System.out.println("线程池关闭超时,强制关闭...");
orderThreadPool.shutdownNow();
// 4. 再等30秒
if (!orderThreadPool.awaitTermination(30, TimeUnit.SECONDS)) {
System.err.println("线程池强制关闭失败!");
}
}
} catch (InterruptedException e) {
// 捕获中断信号,再次强制关闭
orderThreadPool.shutdownNow();
Thread.currentThread().interrupt(); // 恢复中断状态
}
System.out.println("订单线程池关闭完成,状态:" + (orderThreadPool.isTerminated() ? "TERMINATED" : "未终止"));
}
}
2. 进阶方案:结合 Spring Boot 监控线程池状态
生产环境中,需要实时监控线程池状态(比如状态、活跃线程数、队列任务数),这里用 "Spring Boot Actuator + Prometheus + Grafana" 实现:
步骤 1:加依赖(pom.xml)
xml
<!-- Actuator:暴露监控端点 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Prometheus:指标收集 -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
步骤 2:配置监控端点(application.yml)
yaml
management:
endpoints:
web:
exposure:
include: prometheus,health,info # 暴露prometheus端点
metrics:
export:
prometheus:
enabled: true
# 自定义线程池监控指标
enable:
jvm: true
thread-pool: true # 开启线程池监控
步骤 3:自定义线程池监控(暴露状态指标)
java
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.binder.jvm.ExecutorServiceMetrics;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Configuration
public class ThreadPoolConfig {
@Bean(name = "orderThreadPool")
public ThreadPoolExecutor orderThreadPool(MeterRegistry meterRegistry) {
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
5, // 核心线程数
10, // 最大线程数
60, // 空闲线程存活时间
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100), // 任务队列
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
// 注册线程池监控指标(包括状态、活跃数、队列数等)
new ExecutorServiceMetrics(
threadPool,
"order_thread_pool", // 指标前缀
"order_service" // 标签(区分不同服务的线程池)
).bindTo(meterRegistry);
return threadPool;
}
}
步骤 4:Grafana 配置面板
启动服务后,Prometheus 会收集线程池指标(比如jvm_thread_pool_state
,用 0/1 表示是否处于某个状态),在 Grafana 中配置面板,就能实时看到:
-
线程池当前状态(RUNNING/SHUTDOWN 等);
-
活跃线程数、队列任务数、完成任务数;
-
状态流转历史(比如什么时候从 RUNNING→SHUTDOWN)。
这样就能提前发现线程池状态异常,比如 "线程池突然进入 STOP 状态",及时排查是否有人误调用了shutdownNow()
。
五、八年开发的避坑指南:这些状态误区别再踩!
-
误区 1:调用 shutdown () 后线程池立刻关闭
错!shutdown () 只是让线程池进入 SHUTDOWN 状态,会继续执行队列中的任务,直到所有任务跑完才会进入 TERMINATED。如果想等关闭完成,必须调用
awaitTermination()
。 -
误区 2:shutdownNow () 能中断所有任务
错!shutdownNow () 是通过
Thread.interrupt()
中断线程,但如果任务中没有处理InterruptedException
(比如没有 try-catch,或 catch 后没做处理),线程不会真的中断,会继续执行任务。正确做法:任务中处理中断信号,比如:
csharpclass OrderTask implements Runnable { @Override public void run() { try { // 模拟任务执行 for (int i = 0; i < 100; i++) { // 检查是否被中断,是则退出 if (Thread.currentThread().isInterrupted()) { System.out.println("任务被中断,退出执行"); return; } Thread.sleep(100); } } catch (InterruptedException e) { // 恢复中断状态,让上层感知 Thread.currentThread().interrupt(); System.out.println("任务被中断,退出执行"); } } }
-
误区 3:线程池处于 SHUTDOWN 状态还能提交任务
错!SHUTDOWN 状态不接收新任务,提交会抛
RejectedExecutionException
。如果业务需要 "关闭后还能提交紧急任务",可以自定义拒绝策略,比如:typescriptnew ThreadPoolExecutor.CallerRunsPolicy() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { // 如果线程池处于SHUTDOWN状态,用主线程执行紧急任务 if (!e.isShutdown()) { super.rejectedExecution(r, e); } else { System.out.println("线程池已关闭,主线程执行紧急任务"); r.run(); } } }
-
误区 4:isShutdown () 返回 true 表示线程池已关闭
错!isShutdown () 返回 true 只表示线程池处于 SHUTDOWN/STOP/TIDYING/TERMINATED 中的一种,不代表已完全关闭。判断是否完全关闭,必须用
isTerminated()
。
六、总结:理解状态,才能用好线程池
线程池的 5 种状态,本质是 "线程池生命周期的不同阶段"------ 从 RUNNING 的 "忙碌工作",到 SHUTDOWN 的 "收尾",再到 TERMINATED 的 "终点",每个阶段都有明确的行为边界。
八年开发的经验总结:
-
日常开发:关注 RUNNING 状态,确保线程池能正常接收和执行任务;
-
服务停机:用 SHUTDOWN+awaitTermination () 实现优雅停机,避免任务丢失;
-
紧急故障:用 STOP+shutdownNow () 强制恢复,但要处理好任务中断;
-
监控运维:实时监控状态流转,提前发现异常(比如突然进入 STOP 状态)。
其实线程池不难,难的是 "知其然更知其所以然"------ 理解了状态,你就能轻松排查 "任务被拒绝""线程池关不掉" 等问题,成为真正的线程池高手。