聊聊线程池中哪几种状态,分别表示什么?8 年 Java 开发:从业务踩坑到源码拆解(附监控实战)

聊聊线程池中哪几种状态,分别表示什么?8 年 Java 开发:从业务踩坑到源码拆解(附监控实战)

作为一名摸爬滚打 8 年的 Java 开发,踩过最多的线程池坑,几乎都和 "状态" 有关:

  • 服务优雅停机时,调用shutdown()后还在提交任务,结果被拒绝;

  • 线程池明明有空闲线程,却不处理队列任务,查了半天才发现是处于STOP状态;

  • 监控面板上 "线程池活跃数" 一直不变,最后定位到线程池已悄悄进入TERMINATED状态。

其实线程池的所有行为(接受任务、执行任务、关闭)都和 "状态" 强绑定 ------ 理解了这 5 种状态,才算真的懂线程池。今天不聊空洞的理论,只从业务场景 + 源码解析 + 实战监控出发,带你吃透线程池状态的来龙去脉。

一、先抛业务痛点:为什么要关注线程池状态?

新手常觉得 "线程池状态是底层细节,不用管",但八年开发的经验是:90% 的线程池问题,根源都是对状态理解不清。比如这 3 个典型场景:

  1. 场景 1:服务停机时任务丢失

    线上服务重启前,没等线程池处理完队列任务就强制 kill 进程 ------ 其实如果知道SHUTDOWN状态会 "处理完队列任务再关闭",调用shutdown()后等isTerminated()返回true再停机,就能避免任务丢失。

  2. 场景 2:任务提交被拒绝却找不到原因

    明明线程池最大线程数没满、队列也没满,提交任务却抛RejectedExecutionException------ 查源码才发现,线程池之前被调用过shutdownNow(),已进入STOP状态,拒绝接收任何新任务。

  3. 场景 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()方法后,线程池从RUNNINGSHUTDOWN

  • 源码关键逻辑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()方法后,线程池从RUNNINGSTOP(如果是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" 时,会从SHUTDOWNSTOPTIDYING

    • 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 条核心路径即可:

  1. 正常关闭路径RUNNINGSHUTDOWNTIDYINGTERMINATED

    • 触发条件:调用shutdown(),且队列任务执行完、工作线程清空。
    • 业务场景:服务优雅停机。
  2. 紧急关闭路径RUNNINGSTOPTIDYINGTERMINATED

    • 触发条件:调用shutdownNow(),清空队列、中断所有线程,工作线程清空。
    • 业务场景:服务紧急重启、任务有 bug 需立刻停止。
  3. 特殊路径 1SHUTDOWNSTOPTIDYINGTERMINATED

    • 触发条件:调用shutdown()后(已进入 SHUTDOWN),又调用shutdownNow()
    • 业务场景:优雅停机过程中,发现必须立刻停止(比如超时)。
  4. 特殊路径 2RUNNINGSHUTDOWNSTOPTIDYINGTERMINATED

    • 触发条件:调用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. 误区 1:调用 shutdown () 后线程池立刻关闭

    错!shutdown () 只是让线程池进入 SHUTDOWN 状态,会继续执行队列中的任务,直到所有任务跑完才会进入 TERMINATED。如果想等关闭完成,必须调用awaitTermination()

  2. 误区 2:shutdownNow () 能中断所有任务

    错!shutdownNow () 是通过Thread.interrupt()中断线程,但如果任务中没有处理InterruptedException(比如没有 try-catch,或 catch 后没做处理),线程不会真的中断,会继续执行任务。

    正确做法:任务中处理中断信号,比如:

    csharp 复制代码
    class 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. 误区 3:线程池处于 SHUTDOWN 状态还能提交任务

    错!SHUTDOWN 状态不接收新任务,提交会抛RejectedExecutionException。如果业务需要 "关闭后还能提交紧急任务",可以自定义拒绝策略,比如:

    typescript 复制代码
    new 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. 误区 4:isShutdown () 返回 true 表示线程池已关闭

    错!isShutdown () 返回 true 只表示线程池处于 SHUTDOWN/STOP/TIDYING/TERMINATED 中的一种,不代表已完全关闭。判断是否完全关闭,必须用isTerminated()

六、总结:理解状态,才能用好线程池

线程池的 5 种状态,本质是 "线程池生命周期的不同阶段"------ 从 RUNNING 的 "忙碌工作",到 SHUTDOWN 的 "收尾",再到 TERMINATED 的 "终点",每个阶段都有明确的行为边界。

八年开发的经验总结:

  • 日常开发:关注 RUNNING 状态,确保线程池能正常接收和执行任务;

  • 服务停机:用 SHUTDOWN+awaitTermination () 实现优雅停机,避免任务丢失;

  • 紧急故障:用 STOP+shutdownNow () 强制恢复,但要处理好任务中断;

  • 监控运维:实时监控状态流转,提前发现异常(比如突然进入 STOP 状态)。

其实线程池不难,难的是 "知其然更知其所以然"------ 理解了状态,你就能轻松排查 "任务被拒绝""线程池关不掉" 等问题,成为真正的线程池高手。

相关推荐
赵星星5202 分钟前
synchronized锁的是谁,看完这篇就明白了
java·后端
小张学习之旅4 分钟前
线程池简单讲解
java·后端
李九四13 分钟前
章节11:使用括号()标明优先级
后端
叫我阿柒啊17 分钟前
Java全栈开发面试实战:从基础到微服务的深度解析
java·jvm·微服务·vue3·springboot·全栈开发·restfulapi
拾光师18 分钟前
flume监控目录文件实战:三种 Source 方案对比与配置指南
后端
金銀銅鐵22 分钟前
[Java] 浅析注解是如何实现的
后端
李九四24 分钟前
章节10:支持连续的乘除法
后端
精神领袖本尊26 分钟前
ESP8266(esp-01s) 继电器控制灯具项目
后端
cxyxiaokui00126 分钟前
检索增强生成(RAG):打破模型知识壁垒的革命性架构
java·aigc
Monly2132 分钟前
Docker:部署Java后端
java·docker·容器