首页 / Java 学习笔记 / 11

JAVA · Vol.II · DAY 11 · 并发编程

线程生命周期:6 种状态与 12 次转换

中级高频#并发#基础
第 1 站

从一道面试题开始

面试官双手交叉,不紧不慢地抛出这个问题:

"说说线程的生命周期?它有哪些状态?状态之间怎么转换?"

候选人 A 脱口而出:"有 NEW、RUNNING、BLOCKED、WAITING、TERMINATED 五种"。面试官微微一笑——漏了一个,还多了一个(Java 里没有 RUNNING)。

候选人 B 胸有成竹:"六种!NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED"。面试官追问:那状态之间的转换分别由什么 API 触发?候选人沉默了。

再往深挖一层,这是生产排查的基本功:凌晨服务变慢,jstack 抓个线程转储,满屏都是 BLOCKED (on object monitor)——如果你不熟状态机,这屏输出对你就只是一堆英文。

这道题之所以高频,是因为它同时考察了三件事:

  • 你是否真正读过 Thread.State 的源码
  • 你是否理解每种状态背后的操作系统调度原理
  • 你能否把状态转换具体 API 一一对应,并用 jstack 在生产里验证

发车之前,先问自己三个问题:

  • RUNNABLE 到底包不包括"正在等待 CPU 时间片"的线程?
  • BLOCKED 和 WAITING 的本质区别是什么?
  • Thread.sleep()Object.wait() 分别进入什么状态?释放锁吗?

带着这些问题,我们开始今天的线程之旅。

第 2 站

Thread.State 枚举:JDK 源码怎么说

很多人背过线程状态,但很少有人翻开 JDK 源码看一看。线程的所有状态定义在 java.lang.Thread.State 这个枚举中:

java/lang/Thread.java(JDK 17)
public enum State {
    NEW,            // Thread object created but start() not yet called.
    RUNNABLE,       // Executing in the JVM — may be waiting for OS resources (e.g. CPU).
    BLOCKED,        // Waiting to acquire a monitor lock (synchronized block/method).
    WAITING,        // Waiting indefinitely for another thread's action (wait, park, join).
    TIMED_WAITING,  // Waiting up to a specified time (sleep, wait(ms), parkNanos).
    TERMINATED      // The thread has completed execution.
}

注意两个容易被忽略的细节:

细节 1RUNNABLE 不等于"正在执行"。JDK 注释明确写了 "may be waiting for OS resources such as processor time"——也就是说,正在等待 CPU 时间片的线程,状态也是 RUNNABLE。Java 没有单独的 READY / RUNNING 区分。
细节 2BLOCKED 专指等待 synchronized 监视器锁。如果你用的是 ReentrantLock.lock(),线程进入的是 WAITING 或 TIMED_WAITING(底层走 LockSupport.park),而不是 BLOCKED。
记住Thread.State 只有 6 个值,不是 5 个也不是 7 个。面试时说"5 种"直接扣分,说"有 RUNNING"也算错。
第 3 站

完整状态机:6 个状态 × 12 次转换

下面这张图是线程生命周期的完整状态机。每一条箭头对应一次状态转换,箭头上标注了触发的 API。建议你放大仔细看——面试时能画出这张图,基本满分。

Java 线程完整状态机 — 6 状态 · 12 转换 NEW RUNNABLE BLOCKED WAITING TIMED_WAITING TERMINATED ① start() ② 进入 synchronized 块但锁被占用 ③ 获取到锁 ④ Object.wait() LockSupport.park() Thread.join() ⑤ notify/notifyAll 需重新竞争锁 ⑥ unpark/notify(无锁竞争) ⑦ Thread.sleep(ms) Object.wait(ms) LockSupport.parkNanos() Thread.join(ms) ⑧ 超时 / 被唤醒 ⑨ wait(ms) 超时后需重新竞争锁 ⑩ run() 执行完毕 ⑪ 被中断 → 异常抛出 run() 结束 ⑫ 被中断 → 异常抛出 run() 结束 初始态 运行态 阻塞态 等待态 限时等待态 终止态
图 1Java 线程完整状态机:6 个状态、12 次转换。每条箭头旁标注了触发转换的 API。

将 12 次转换整理成表格:

#转换触发 API
NEW → RUNNABLEThread.start()
RUNNABLE → BLOCKED进入 synchronized 块,锁被其他线程持有
BLOCKED → RUNNABLE成功获取到监视器锁
RUNNABLE → WAITINGObject.wait() / LockSupport.park() / Thread.join()
WAITING → BLOCKEDnotify() / notifyAll(),唤醒后需重新竞争锁
WAITING → RUNNABLELockSupport.unpark(),无需竞争锁直接进入就绪
RUNNABLE → TIMED_WAITINGThread.sleep(ms) / Object.wait(ms) / Thread.join(ms) / LockSupport.parkNanos()
TIMED_WAITING → RUNNABLE超时自动恢复 / 被 unpark() 唤醒
TIMED_WAITING → BLOCKEDObject.wait(ms) 超时或唤醒后需重新竞争 synchronized 锁
RUNNABLE → TERMINATEDrun() 方法正常返回或抛出未捕获异常
WAITING → TERMINATEDThread.interrupt() → 抛 InterruptedException 未被捕获 → 冲出 run() → 线程死亡
TIMED_WAITING → TERMINATED同上(sleep / join(ms) / wait(ms) 中被打断)
注意 ⑪⑫ 的细节:被中断的 WAITING 线程不是"直接被中断杀死",而是 wait()/park() 抛出 InterruptedException,这个异常如果一路未捕获冲出 run(),线程才进入 TERMINATED。如果业务代码 catch 住了继续跑,线程还活着(只是中断标志被清掉了)。另外 BLOCKED 状态不能响应 interrupt——等 synchronized 锁的线程被打断只是记下标志,照样排队,拿到锁之后代码才有机会检查标志。
状态机核心公式:状态 = f(最近一次 API 调用)——每次转换都由一个明确的 API 触发,不存在"隐式"转换。
第 4 站

六种状态逐一拆解 + 代码验证

状态含义进入方式(代码示例)退出条件
NEW对象已创建,尚未调用 start()Thread t = new Thread();调用 t.start()
RUNNABLE正在运行 或 等待 CPU 调度t.start()、从阻塞返回获取不到锁 / 主动等待 / 执行完毕
BLOCKED等待获取 synchronized 监视器锁进入 synchronized 块但锁被其他线程持有获取到锁 → RUNNABLE
WAITING无限期等待另一个线程的动作Object.wait()LockSupport.park()Thread.join()被 notify / unpark → RUNNABLE 或 BLOCKED
TIMED_WAITING限时等待,到时间自动恢复Thread.sleep(ms)Object.wait(ms)LockSupport.parkNanos()超时 / 被唤醒 → RUNNABLE 或 BLOCKED
TERMINATEDrun() 方法执行完毕run() 正常返回或抛出未捕获异常终态,不可再转换

下面用代码验证每个状态:

ThreadStateDemo.java
// 1. NEW
Thread t1 = new Thread(() -> {});
System.out.println(t1.getState()); // NEW

// 2. RUNNABLE
Thread t2 = new Thread(() -> {
    while (true) { Thread.yield(); }
});
t2.start();
System.out.println(t2.getState()); // RUNNABLE

// 3. BLOCKED
Object lock = new Object();
Thread holder = new Thread(() -> {
    synchronized (lock) {
        try { Thread.sleep(999999); } catch (Exception e) {}
    }
});
holder.start();
Thread.sleep(100); // 等 holder 拿到锁
Thread t3 = new Thread(() -> {
    synchronized (lock) {} // 拿不到锁 → BLOCKED
});
t3.start();
Thread.sleep(100);
System.out.println(t3.getState()); // BLOCKED

// 4. WAITING
Thread t4 = new Thread(() -> {
    synchronized (lock) {
        try { lock.wait(); } catch (Exception e) {}
    }
});
// ... start + sleep → WAITING

// 5. TIMED_WAITING
Thread t5 = new Thread(() -> {
    try { Thread.sleep(60000); } catch (Exception e) {}
});
t5.start();
Thread.sleep(100);
System.out.println(t5.getState()); // TIMED_WAITING

// 6. TERMINATED
Thread t6 = new Thread(() -> {});
t6.start();
t6.join();
System.out.println(t6.getState()); // TERMINATED
核心每种状态都有明确的"入口 API"和"出口条件"。面试不是背名词,而是能说清楚什么代码产生什么状态
第 5 站

RUNNABLE 深挖:一个 JVM 状态,两个 OS 状态

第 2 站埋了个细节:RUNNABLE 包含"等待 CPU 时间片"。这句话值得单独展开,因为它揭示了 JVM 状态和 OS 状态不是一一对应的

JVM 状态对应的 OS 线程状态说明
NEWOS 线程还没创建start() 之前,只有 Java 对象,没有底层 pthread
RUNNABLERunning(正在 CPU 上执行)占用一个 CPU 核,正在跑 run()
 Ready(就绪,在运行队列里排队)时间片用完了 / 被更高优先级线程抢占了,但随时可被调度
BLOCKEDOS 层面阻塞(等 JVM 的 monitor,JVM 内部排队)注意:不是 OS 的 futex 阻塞,是 JVM 的 ObjectMonitor 管理
WAITING / TIMED_WAITINGOS 层面阻塞(futex / 条件变量)park 会真正让 OS 线程睡掉,不占 CPU
TERMINATEDOS 线程退出pthread 已 join 回收

三个推论,全是面试加分点:

  • "jstack 里看到 RUNNABLE" ≠ 线程在跑。8 核机器上有 20 个 RUNNABLE 线程,同一时刻只有 8 个真的在 CPU 上,另外 12 个在 OS 运行队列里排队。CPU 密集型服务线程数远超核数时,大部分 RUNNABLE 线程其实都在排队——这是「线程越多吞吐越低」的底层原因。
  • RUNNABLE 线程不会被「暂停」:时间片切换对 Java 代码完全透明,你的代码没有任何一行能观察到「我刚被抢占过」——所以 Java 层面不区分 READY/RUNNING,区分了也没用。
  • yield() 只是「让一让」:把当前线程从 Running 挪回 Ready 队尾,不保证下次立刻再调度回来,别把它当同步手段用(第 4 站 demo 里用它只是制造一个可观测的 RUNNABLE 样本)。
面试金句"Java 的 RUNNABLE 是一个『容器状态』,装的是 OS 的 Running + Ready 两个状态。JVM 不做这个区分,因为 Java 代码无法观测也无法影响 OS 调度——能观测的只有『我能不能跑到』,而这由 OS 决定。"
第 6 站

NEW → RUNNABLE:start() 和 run() 的本质区别

状态机的第一条边(①)背后是新手最常踩的坑:直接调 run() 不会创建新线程

RunVsStart.java —— 两种写法,两种命运
Thread t = new Thread(() ->
    System.out.println("线程名: " + Thread.currentThread().getName()));

// 写法 1:直接调 run() —— 普通方法调用,还是 main 线程在跑
t.run();   // 输出:线程名: main,状态永远是 NEW

// 写法 2:调 start() —— JVM 创建 OS 线程,在新线程里执行 run()
Thread t2 = new Thread(t::run);
t2.start(); // 输出:线程名: Thread-1(新线程!)

// 写法 3:对同一线程对象调两次 start()
t2.start(); // IllegalThreadStateException:线程是一次性的

为什么这样设计?start() 内部做的是重量级的事:创建底层 OS 线程(pthread)、分配栈内存、把线程状态从 NEW(threadStatus=0)翻成 RUNNABLE、再让 OS 调度器接管。这些工作只该发生一次:start() 开头就检查 threadStatus != 0 直接抛异常——这也是为什么线程不能「重启」,跑完的线程对象只能当尸体用,要再用只能 new 一个。

run() 只是个普通方法,谁调谁执行。调 run() 的经典错误后果:你以为并行跑了 100 个任务,其实是在一个线程里串行了 100 遍,总耗时变成 100 倍——而且不报任何错,静默变慢。正确姿势永远是用线程池的 execute/submit(见「线程池」篇),而不是手动 start。

追问:线程创建到底花了多少成本?创建 OS 线程要分配 1MB(默认栈大小)的栈空间 + 线程控制块 + 一次系统调用(clone),一次大约几百微秒到毫秒级,且线程数是硬资源——创建 1 万个线程的机器基本就废了。这就是「线程池」存在的全部理由:贵,所以要复用。
第 7 站

RUNNABLE → BLOCKED:synchronized 的排队机制

一个 RUNNABLE 线程撞上被别人持有的 synchronized 锁,就进入 BLOCKED,在锁的 EntryList(ObjectMonitor 的阻塞列表)里排队。几个必须讲清楚的细节:

  • 排队不等于 FIFO:synchronized 没有公平性保证,持锁线程释放后,JVM 从 EntryList 里挑一个唤醒(实现上不保证先排的先拿到),其他线程继续等。所以 synchronized 下的锁是「非公平」的——想公平,用 ReentrantLock(true)
  • BLOCKED 期间不响应 interrupt:中断只是记下标志位,线程照样排队,直到拿到锁进入临界区后,代码才可能检查标志(第 12 站细讲)。
  • 持锁线程在临界区里 wait() 了会怎样:wait 会释放锁,排队的 BLOCKED 线程就能拿锁了——这就是为什么「持锁 + wait」是合法的,否则所有等这个锁的人全饿死。

jstack 里 BLOCKED 线程长这样(第 13 站展开):

jstack 里的 BLOCKED
"worker-1" #11 ...
  waiting for monitor entry
  java.lang.Thread.State: BLOCKED (on object monitor)
    at com.example.OrderService.process(OrderService.java:42)
    - waiting to lock <0x000000076bf62208> (a java.lang.Object)

waiting to lock <地址> 就是线索:拿这个地址去全文搜 - locked <同一地址>,就找到了持锁者——谁持有、持有者卡在哪一行,锁竞争的全貌就出来了。

第 8 站

RUNNABLE → WAITING:wait / join / park 三兄弟

三个 API,同一个状态(WAITING),但语义和用法差别很大:

API等什么谁唤醒前提条件典型用途
obj.wait()等另一个线程调 notify/notifyAllnotify 挑一个,notifyAll 全唤醒必须持有 obj 的锁,否则 IllegalMonitorStateException生产者-消费者、条件等待
t.join()等线程 t 结束t 的 run() 结束(内部等价于对 t.wait())无(任何线程可调用)主线程等子任务完成
LockSupport.park()等 unpark 信号(许可)LockSupport.unpark(t)无(不需要持锁)AQS 底层、线程池工作线程取任务

三个易错点:

  • wait() 是实例方法,sleep() 是静态方法Object.wait() 操作的是「这个对象的监视器」,Thread.sleep() 只是让当前线程睡——所以 wait 必须在 synchronized 块里(要操作 monitor),sleep 在哪都能调。
  • park 是「许可制」:unpark 可以在 park 之前调用(许可先存着,后面 park 直接通过)——这比 wait/notify「先 wait 后 notify 才能匹配」灵活得多,AQS 的队列唤醒全靠它。
  • wait() 会释放锁,sleep() 不会:这是「持锁线程想让路」时唯一正确的睡法——sleep 着持锁,所有竞争者全 BLOCKED,直接堵死。
第 9 站

RUNNABLE → TIMED_WAITING:四个限时 API 的取舍

API释放锁?响应中断?超时行为适合场景
Thread.sleep(ms)不释放是(抛 InterruptedException)到点自动恢复简单延时、轮询间隔
obj.wait(ms)释放到点自动恢复,重新竞争锁持锁线程限时等条件
t.join(ms)不涉及锁超时后不再等 t限时等任务完成
LockSupport.parkNanos(ns)不涉及锁是(但不清许可)到点或 unpark 恢复框架底层(AQS、线程池)
最容易出 bug 的场景:持锁 sleep
// ❌ 反例:持锁 sleep —— 10 秒内所有竞争线程全部 BLOCKED
synchronized (lock) {
    Thread.sleep(10_000);   // 锁一直攥在手里!
    doWork();
}

// ✅ 正解 1:真的需要等条件 → wait(释放锁)
synchronized (lock) {
    while (!condition) lock.wait(10_000);  // 等的时候锁放出去了
    doWork();
}

// ✅ 正解 2:只是任务本身慢 → 别在锁里干慢事
synchronized (lock) {
    submitHeavyTask();   // 锁内只做登记,重活丢给线程池
}
口诀等条件用 wait(放锁),纯延时用 sleep(不放锁但别在锁里睡),等线程用 join,框架底层用 park。
第 10 站

唤醒路径:WAITING 醒来 ≠ 立刻执行

WAITING / TIMED_WAITING 的线程有三条醒来的路:被通知(notify / unpark)、超时被中断(interrupt → InterruptedException)。但「醒来」之后发生什么,是这里最容易被误解的地方:

醒来的三步骤
① 被唤醒 ≠ 继续执行:wait()/wait(ms) 醒来后要先重新竞争 monitor 锁(拿不到就进 BLOCKED,对应状态机 ⑤⑨ 边)
② park() 醒来没有锁的问题,但只是「有资格跑」,还要 OS 调度
③ 条件必须重新检查——醒来不代表条件成立

第③点直接决定了生产代码里一个铁律:wait 必须包在 while 里,不能用 if

为什么必须 while 不能 if
synchronized (lock) {
    // ❌ if:醒来直接往下走,但条件可能早被别人消费掉了
    if (deque.isEmpty()) notEmpty.await();
    T v = deque.removeFirst();   // 可能 remove 一个空队列 → 出错的不是报错,是数据错乱

    // ✅ while:醒来后重新判断,不满足继续等
    while (deque.isEmpty()) notEmpty.await();
    T v = deque.removeFirst();   // 此刻队列一定非空
}

必须 while 的两个原因,面试都要说出来:

  • 虚假唤醒(spurious wakeup):JVM 规范允许 wait() 在没有 notify 的情况下返回(底层 OS/硬件层面的设计),这是语言层面兜底的,业务代码必须自己扛。
  • notify 之后条件可能被抢走:notifyAll 唤醒 5 个消费者,锁释放后 5 个人逐个重新竞争锁,第一个人拿走唯一元素,后四个醒来时条件已经不成立了——if 版代码就会在空队列上翻车。

顺带把 notify() vs notifyAll() 定个调:能确定「只有一个等待者需要这个条件」时 notify 省开销;条件语义是「全体相关方都可能关心」时(如队列容量既约束生产者又约束消费者)必须 notifyAll,漏唤醒比多唤醒致命得多。

第 11 站

wait/notify vs sleep:一张表终结混淆

第 8、9 站分别讲了 wait 家族和 sleep 家族,这一站把它们放在同一张表里对撞——这是面试被追问频率第二高的问题:

维度Object.wait()/notify()Thread.sleep()
所属Object 的实例方法(操作 monitor)Thread 的静态方法(操作当前线程)
释放锁?释放(等的时候把 monitor 让出来)不释放(攥着锁睡)
调用前提必须在 synchronized 块内,否则 IllegalMonitorStateException任何位置都可以
恢复方式notify/notifyAll / 超时 / 中断超时(到点必醒)/ 中断
能否指定时长能(wait(ms) / wait(ms, ns))必须(sleep 就是要睡一会儿)
响应中断是,抛 InterruptedException 且清除中断标志是,抛 InterruptedException 且清除中断标志
本质线程间的协作(一个喊,一个应)线程的自我延时(不需要别人配合)

一句话选型:需要「等某个条件成立」→ wait 家族;只是「过一会儿再来」→ sleep。生产里最常见的错误是拿 sleep 当 wait 用(轮询代替协作,延迟高还浪费 CPU),以及拿 wait 当 sleep 用(忘了 notify,线程永久 WAITING)——两种错误 jstack 里都看得见,一个是 TIMED_WAITING (sleeping),一个是 WAITING (on object monitor) 停了好几天。

第 12 站

interrupt 深挖:它不是「打断」,是「发信号」

"怎么停止一个线程?"——答案永远是:你停不了,你只能发信号,线程自己决定要不要听。这就是 interrupt 的全部真相。

Thread.interrupt(t) 对处于不同状态的线程,行为完全不同——这是「中断」机制最常被答歪的地方:

目标线程状态interrupt 的效果
RUNNABLE(正常跑着)只设置中断标志位(boolean),线程继续跑,什么都不发生——要它自己检查
WAITING(wait/park/join 中)抛出 InterruptedException并清除中断标志
TIMED_WAITING(sleep/join(ms)/wait(ms) 中)同上:抛异常 + 清标志
BLOCKED(等 synchronized)不响应:只记标志,照样排队;拿到锁后代码才有机会看到标志
TERMINATED无效

两个 API 读标志,语义不一样,别混:

isInterrupted() vs interrupted()
Thread.currentThread().isInterrupted();  // 只读不清:看标志还在不在
Thread.interrupted();                   // 读且清:一次性消费(静态方法,只能查当前线程)

// 长循环线程的优雅退出惯例:定期检查标志
while (!Thread.isInterrupted()) {
    processOneTask();   // 单步之间检查,退出延迟 = 单步耗时
}

为什么中断状态要「抛异常 + 清标志」?因为异常已经把信号送达了(你的 catch 里处理),再留着标志会引发二次误伤——下一轮循环 Thread.isInterrupted() 又 true,线程莫名其妙退出。惯例:在 catch 里如果决定继续跑,可以 Thread.currentThread().interrupt() 把标志重新补上,把决定权交还给上层;如果决定退出,就什么都不做让线程自然结束。

生产准则写长循环 / 任务处理代码时,把「响应中断」当成接口契约的一部分:循环头检查 isInterrupted(),可中断点(IO、park、sleep)让异常正常抛出。这样 thread.interrupt() 才是真正的停止按钮,否则它只是一根没人接的电话线。
第 13 站

TERMINATED:正常退场与异常猝死

线程进 TERMINATED 只有两条路:run() 正常返回,或 run() 里抛出未捕获的异常。第二条路比表面看起来更危险:

  • 未捕获异常 = 线程猝死:异常冲出 run() 后,JVM 不会杀死整个进程,只是让这一个线程死亡,并把异常交给 UncaughtExceptionHandler(默认打印到 stderr)。业务后果:你以为是常驻的工作线程,其实早就死了,任务积压却毫无告警——「线程池里线程越来越少」的排查第一步就是看 UncaughtExceptionHandler 日志。
  • 线程对象不可复活:TERMINATED 后 start() 直接抛 IllegalThreadStateException。需要「再来一次」就 new 新线程(或更好的:线程池复用)。
  • 怎么知道它跑了没有 / 跑得对不对t.join() 等它结束(自己进 TIMED_WAITING/WAITING);t.isAlive() 查是否还在跑;有返回值的任务用 FutureTask/线程池 submitfuture.get() 能拿到结果或异常(呼应「线程创建」篇的 execute vs submit 异常处理)。
给工作线程装个「死亡播报」
ThreadFactory factory = r -> {
    Thread t = new Thread(r, "worker-" + seq.getAndIncrement());
    t.setUncaughtExceptionHandler((th, ex) -> {
        log.error("工作线程 {} 意外死亡", th.getName(), ex);
        metrics.inc("thread.death");   // 告警:常驻线程死亡 = 事故
    });
    return t;
};
第 14 站

jstack 实战:从线程转储中读状态

面试官的终极杀招:"给你一个线程转储,你能分析出什么问题?"掌握 jstack 输出格式,是你从"背八股"进阶到"真会用"的标志。

先看一段真实的 jstack 输出(节选):

jstack 输出示例
$ jstack 12345
Full thread dump OpenJDK 64-Bit Server VM (17.0.2+8):

"worker-1" #11 prio=5 os_prio=0 tid=0x00007f3a1c0e8800 nid=0x2a03
  waiting for monitor entry [0x00007f3a08ffe000]
  java.lang.Thread.State: BLOCKED (on object monitor)
    at com.example.OrderService.process(OrderService.java:42)
    - waiting to lock <0x000000076bf62208> (a java.lang.Object)
    at com.example.Worker.run(Worker.java:18)

"worker-2" #12 prio=5 os_prio=0 tid=0x00007f3a1c0ea000 nid=0x2a04
  in Object.wait() [0x00007f3a08efe000]
  java.lang.Thread.State: WAITING (on object monitor)
    at java.lang.Object.wait(Native Method)
    - waiting on <0x000000076bf62208> (a java.lang.Object)
    at java.lang.Object.wait(Object.java:328)
    at com.example.Consumer.consume(Consumer.java:35)

"timer-1" #13 prio=5 os_prio=0 tid=0x00007f3a1c0ec000 nid=0x2a05
  sleeping [0x00007f3a08dfe000]
  java.lang.Thread.State: TIMED_WAITING (sleeping)
    at java.lang.Thread.sleep(Native Method)
    at com.example.Scheduler.poll(Scheduler.java:22)

"main" #1 prio=5 os_prio=0 tid=0x00007f3a1c010800 nid=0x2901
  runnable [0x00007f3a23ffe000]
  java.lang.Thread.State: RUNNABLE
    at com.example.App.main(App.java:15)

逐行解读的关键字段:

字段含义示例
nid操作系统原生线程 ID(十六进制)0x2a03 → 用 top -Hp 查 CPU 占用
Thread.StateJava 层面的线程状态BLOCKED / WAITING / RUNNABLE ...
waiting for monitor entry正在排队等 synchronized 锁对应 BLOCKED
in Object.wait()调用了 wait() 正在等待信号对应 WAITING
waiting to lock <addr>等待获取的锁对象地址和"locked <addr>"配对分析
- locked <addr>当前持有的锁找到谁持有了这把锁

先读「状态分布」,再读「具体线程」——这是老手和新手的第一区别:

  • 大量 WAITING 通常正常:线程池的空闲工作线程 park 在取任务、消费者 wait 在空队列、RPC 线程等 IO——WAITING 是「礼貌地等」,本身不是病。
  • 大量 BLOCKED = 锁竞争热点:几十个线程 waiting to lock 同一个地址,而持锁线程栈顶还卡在某行慢代码(比如锁内调 RPC)——这就是要修的问题:锁粒度太大或锁内干了重活。
  • RUNNABLE 但 CPU 打满:找 nid 对应 top -Hp 的高耗线程,看它是不是在忙等循环(没 sleep 的 while)或 GC(VM 线程)。
  • WAITING 停在同一条 wait() 好几天:被唤醒的 notify 漏发了,或者条件永远不成立——配合 while 条件排查。

确认「某个线程到底卡在哪」的排查三板斧:

排查步骤
# 第一步:导出线程转储(-l 带锁信息)
jstack -l <pid> > thread_dump.txt

# 第二步:统计状态分布,先看大局
grep -o "Thread.State: [A-Z_]*" thread_dump.txt | sort | uniq -c

# 第三步:有 BLOCKED 的话,按锁地址找持有者
grep -A 5 "BLOCKED" thread_dump.txt
grep "0x000000076bf62208" thread_dump.txt
# → 找到 "locked <0x...>" 的线程 = 持锁者,看它的栈顶在干什么

# 如果 jstack 末尾出现 "Found one Java-level deadlock":
# → 确认死锁,按输出的循环依赖链修代码(通常是加锁顺序不一致)
实战技巧间隔 10 秒抓 3 次 jstack 做 diff:连续三次停在同一行的线程 = 真卡死;三次都在动 = 只是慢。对 BLOCKED 集群,重点永远在持锁者而不是排队者——排队的人无辜,攥着锁不放的人才是要审的对象。
第 15 站

生产症状 → 状态 → 病根:速查表

把 14 站的排查经验压成一张「症状速查表」,下次 oncall 直接对号入座:

jstack 症状常见病根处置方向
几十个线程 BLOCKED (on object monitor),waiting to lock 同一地址锁竞争热点:锁内慢代码(RPC/IO/大循环)或锁粒度太大让持锁者栈顶说话:缩小临界区、锁外做慢事、或换无锁/分段方案
线程池线程全 WAITINGArrayBlockingQueue.take()池空闲(正常)或上游断流(异常)对照 QPS 曲线:正常流量=健康;流量还在但没人取任务=提交端故障
业务线程 WAITING (on object monitor) 停在自定义 wait()notify 漏发 / 条件永不成立 / 虚假唤醒没 while 兜底检查 notify 路径是否所有分支都覆盖;wait 改 while
线程 WAITINGSocketRead / read下游 RPC / DB 慢(线程在等 IO 返回)查下游 RT 和连接池;给调用加超时(无超时的阻塞调用是定时炸弹)
大量线程 RUNNABLE,CPU 接近 100%忙等循环 / 死循环 / GC 线程 / 正则回溯(ReDoS)top -Hp 定位 nid → jstack 看栈顶;正则类问题给输入加白名单/长度限制
线程数持续上涨,旧线程不消失线程泄漏:new Thread 没进池 / 池没 shutdown看线程名归类;所有线程收编进命名线程池
常驻工作线程数量只减不增线程猝死:run() 里未捕获异常查 UncaughtExceptionHandler 日志;任务体包 try-catch
jstack 末尾 Found one Java-level deadlock多把锁加锁顺序不一致(A→B vs B→A)统一全局加锁顺序;或缩小到单锁;复杂场景用锁分层
oncall 心法先看状态分布定性质(锁问题 / IO 问题 / CPU 问题),再按锁地址或栈顶定嫌疑人,最后三次 jstack diff 定实锤。状态机是地图,jstack 是罗盘——两个都在手里,oncall 不慌。
总结

这一篇你掌握了什么

把今天的核心知识压缩成一张速查表:

状态一句话关键 API(进入)关键 API(退出)
NEW创建了但没 startnew Thread()start()
RUNNABLE正在跑或排队等 CPU(含 OS Ready)start() / 从其他状态恢复阻塞 / 等待 / 结束
BLOCKED等 synchronized 锁(不响应 interrupt)进入 sync 块但锁被占用拿到锁 / 持锁者释放
WAITING等别人通知(wait/park/join)wait() / park() / join()notify / unpark / 中断
TIMED_WAITING等一会儿,到点自动醒sleep(ms) / wait(ms) / join(ms)超时 / 被唤醒 / 中断
TERMINATED跑完了(或异常猝死),不可逆run() 结束

高频面试追问 & 标准答案:

Q1: sleep() 和 wait() 的区别?

A: 六个维度——sleep 是 Thread 静态方法、wait 是 Object 实例方法;sleep 不释放锁、wait 释放锁;wait 必须在 synchronized 内调用否则 IllegalMonitorStateException、sleep 随处可调;wait 靠 notify/notifyAll/超时/中断恢复、sleep 靠超时/中断;wait 是线程协作(等条件)、sleep 是自我延时;底层 wait 进 monitor 的 WaitSet、sleep 由 JVM 调度挂起。

Q2: BLOCKED 和 WAITING 的区别?

A: BLOCKED 是被动等 synchronized 锁,JVM 自动排队,不响应中断;WAITING 是主动调 wait/park/join 等信号,可被 notify/unpark/interrupt 唤醒。关键推论:ReentrantLock.lock() 拿不到锁产生的是 WAITING(底层 park),不是 BLOCKED——所以 jstack 里一片 WAITING 不一定是问题,一片 BLOCKED 才是锁竞争热点。

Q3: 线程在 wait() 时调 interrupt() 会发生什么?

A: wait() 立即抛出 InterruptedException,同时清除中断标志(异常已送达,标志不必再留)。线程从 WAITING 恢复,要重新竞争锁(拿不到就 BLOCKED)。如果业务代码不 catch 让它冲出 run(),线程就进 TERMINATED——这就是状态机里 WAITING → TERMINATED 那条边。

Q4: 如何正确中断一个线程?

A: 调用 Thread.interrupt()。如果目标线程处于 sleep / wait / join(TIMED_WAITING 或 WAITING),会立即抛出 InterruptedException 并清除中断标志。如果目标线程正在 RUNNABLE 状态运行,只设置中断标志位,需要目标线程自己检查 Thread.interrupted() 并退出。所以「可中断」是线程作者的契约:长循环要检查标志,可中断点要让异常正常传播。

核心知识点回顾

  • Thread.State 有且仅有 6 个值:NEW / RUNNABLE / BLOCKED / WAITING / TIMED_WAITING / TERMINATED。
  • 状态之间有 12 次转换,每次都由明确的 API 触发(能徒手画出状态机图)。
  • RUNNABLE = OS 的 Running + Ready;jstack 里的 RUNNABLE 不等于在跑。
  • start() 创建 OS 线程(一次性),run() 是普通方法调用。
  • BLOCKED 专等 synchronized 锁、非公平、不响应中断;ReentrantLock 等锁是 WAITING。
  • wait 三兄弟:wait(放锁+协作)、park(许可制)、join(等线程);sleep 不放锁。
  • 唤醒 ≠ 执行:wait 醒来要重竞争锁,条件必须 while 重新检查(虚假唤醒 + 抢跑)。
  • interrupt 是发信号不是打断:RUNNABLE 只设标志,WAITING 抛异常清标志,BLOCKED 不响应。
  • 未捕获异常 = 线程猝死:给常驻线程装 UncaughtExceptionHandler + 打点告警。
  • jstack:先读状态分布再读单线程;大量 WAITING 通常正常,大量 BLOCKED = 锁热点。

一句话总结

线程状态机是「API 的投影」:每个状态都是某个调用(start/wait/park/sleep/同步失败)的指纹,jstack 就是拿着指纹库读现场——状态分布定性质,锁地址和栈顶定嫌疑人。

🎤 面试 30 秒总结

"Java 线程有 6 个状态:NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED。RUNNABLE 包含 OS 层的 Running 和 Ready——JVM 不区分,因为 Java 代码观测不到调度。12 次转换都由明确 API 触发:start 进 RUNNABLE;synchronized 抢锁失败进 BLOCKED(不响应中断);wait/park/join 进 WAITING,sleep/wait(ms)/join(ms) 进 TIMED_WAITING。唤醒后 wait 家族要重新竞争锁,所以条件必须 while 检查,防虚假唤醒和抢跑。BLOCKED 和 WAITING 的本质区别是「被动等锁 vs 主动等信号」,ReentrantLock 等锁走 park 所以是 WAITING 不是 BLOCKED。生产上 jstack 先看状态分布:大量 WAITING 通常正常,大量 BLOCKED 找同一锁地址的持锁者审栈顶;线程死亡要看 UncaughtExceptionHandler。"

Comments · 评论