线程生命周期与状态转换
候选人小李在面试美团 P6 时,面试官看了一眼简历上的"多线程开发经验",问道:
"Java 线程有哪些状态?它们之间怎么转换的?"
小李想了想:"有创建、运行、阻塞...还有等待?"面试官追问:"那 WAITING 和 TIMED_WAITING 有什么区别?BLOCKED 和 WAITING 呢?"
小李犹豫了:"它们都是...不运行的状态?"面试官继续追问:"一个线程在 synchronized 锁竞争失败后是什么状态?调用 Lock.lock() 失败后呢?"
小李彻底卡住了...
一、核心问题:线程的六种状态 🔴
1.1 问题拆解
这道题考察的是候选人对 Java 并发模型的系统理解,而不是背诵 Thread.State 枚举。面试官的追问链通常如下:
1.2 ❌ 错误示范
候选人原话 A:"线程有 NEW、RUNNING、BLOCKED、WAITING、TERMINATED 五种状态。"
问题诊断:RUNNING 不是 Java 线程状态。Java 中的可运行状态是 RUNNABLE,它涵盖了 Ready(就绪)和 Running(运行)两个操作系统层面的状态。这是最常见的错误——混淆了 JVM 层和 OS 层的线程状态。
候选人原话 B:"调用 sleep() 会释放锁,调用 wait() 也会释放锁。"
问题诊断:这是典型的错误认知。wait() 会释放锁,但 sleep() 不会。两者在是否释放 CPU 时间片上类似,但在锁语义上完全不同。面试官追问"那调用 sleep() 时持有的是哪个锁",答不上来说明没踩过坑。
候选人原话 C:"BLOCKED 就是等待锁,WAITING 就是等待其他线程。"
问题诊断:这个理解太粗糙。BLOCKED 是 synchronized 锁竞争失败后的被动等待,WAITING 是线程主动调用 Object.wait()、LockSupport.park() 等方法后的状态。两者的唤醒机制和超时支持完全不同。
1.3 标准回答
P5 级别:准确列举六种状态
Java 线程有六种状态,定义在
java.lang.Thread.State枚举中:
- NEW:线程被创建但尚未启动(未调用
start()方法)- RUNNABLE:可运行状态,包括 OS 层面的 Ready(就绪)和 Running(运行)
- BLOCKED:阻塞状态,等待获取
synchronized排他锁- WAITING:等待状态,线程主动调用
wait()、join()、LockSupport.park()等方法- TIMED_WAITING:超时等待,与 WAITING 类似但有超时参数
- TERMINATED:终止状态,线程执行完毕
一个关键点:Java 的
RUNNABLE状态不等于"正在 CPU 上执行"。当线程调用阻塞 I/O 操作(如ServerSocket.accept())时,在 OS 层面是阻塞的,但在 JVM 层面仍然是RUNNABLE——因为 JVM 认为线程还在"等待 I/O 完成"这个事件,这个等待对 JVM 是不可见的。
这个回答展示了准确的概念和关键细节,已经比大多数候选人强了。
P6 级别:掌握完整状态转换图
线程状态的完整转换如下:
BLOCKED vs WAITING 的核心区别:
这个回答能画出完整状态图并解释转换条件,已经达到 P6 要求。
P7 级别:深入源码与 JVM 实现
如果你想在面试中拉开差距,需要理解 JVM 层面是如何实现这些状态的。
NEW → RUNNABLE:调用
start()后,线程被加入 JVM 的调度队列,实际何时上 CPU 由 OS 调度器决定。RUNNABLE → BLOCKED:
synchronized锁由对象头中的 Mark Word 记录持有线程。当线程尝试获取已被占用的锁时,JVM 会将线程挂到该锁的 Entry Set 中,线程进入 BLOCKED 状态。锁释放时:JVM 从 Entry Set 中唤醒一个线程(通常是等待时间最长的),该线程从 BLOCKED 变为 RUNNABLE。RUNNABLE → WAITING:调用
wait()后,线程从 Entry Set 移动到该对象的 Wait Set(等待队列),并释放已持有的锁。Wait Set 由 ObjectMonitor 管理,底层是双向链表。BLOCKED vs WAITING 的唤醒机制差异:BLOCKED 线程在锁释放后立即被唤醒(无需额外信号),而 WAITING 线程必须收到
notify()或notifyAll()才会被移动到 Entry Set(此时仍需竞争锁)。一个生产中的高频陷阱:
Lock.lock()失败后线程是什么状态?答案是 WAITING(LockSupport.park()),不是 BLOCKED。因为Lock使用 AQS 的AbstractQueuedSynchronizer,线程失败后被 park,状态为 WAITING/TIMED_WAITING。这与synchronized的 BLOCKED 完全不同——这也是为什么jstack中能看到线程处于WAITING on <monitor>或WAITING on java.util.concurrent.locks两种截然不同的等待。
【面试官心理】 我问他线程状态,其实是在探测他对 JVM 和 OS 交互的理解程度。90% 的候选人知道六种状态,但说不清 BLOCKED 和 WAITING 在 JVM 层面的实现差异。能说出 ObjectMonitor、Entry Set、Wait Set 的,说明他看过 JVM 源码,这是 P6/P7 的分界线。
1.4 追问升级
追问 1:sleep(0) 和 yield() 有什么区别?
Thread.sleep(0)表示放弃本次时间片剩余部分,重新进入就绪队列等待调度。Thread.yield()同样是建议调度器让出 CPU,但具体行为取决于 OS 实现。两者都不释放锁。实战场景:
sleep(0)可用于忙轮询中的"让出"——在自旋等待某个条件时,用sleep(0)避免 CPU 占用 100%,但这个做法通常不如LockSupport.parkNanos()优雅。
追问 2:为什么 wait() 必须在 synchronized 中调用?
这是 Java 设计层面的约束。
wait()依赖对象头的 monitor 锁机制,必须在持有 monitor 的情况下调用,否则抛出IllegalMonitorStateException。这背后的设计哲学是:等待一个条件之前必须先持有锁,否则唤醒和等待之间会产生竞态条件。
追问 3:线程池中的线程是什么状态?
线程池中的线程在
getTask()阻塞等待任务时,通常处于WAITING或TIMED_WAITING状态(取决于队列是否有超时配置)。关键:线程池中的线程不会直接变成 TERMINATED,而是会循环利用——这是线程池的核心价值:复用线程,避免频繁创建/销毁的开销。
二、BLOCKED 与 Monitor 的关系 🟡
2.1 synchronized 锁的底层实现
在 JDK 8 中,
synchronized的底层实现依赖 ObjectMonitor:线程竞争
synchronized锁时:
- 通过 CAS 尝试将
_owner从 NULL 设为当前线程(偏向锁/轻量锁路径)- 失败后进入
_EntryList,状态变为 BLOCKED- 锁释放时唤醒
_EntryList中的一个线程
2.2 Lock 的底层实现差异
ReentrantLock使用 AQS,不依赖对象头的 monitor。AQS 内部有volatile int state和一个双向链表作为等待队列。
lock()失败后,线程被封装为 Node 加入等待队列,并调用LockSupport.park()挂起,状态为 WAITING(精确说是Node.SHARED或Node.EXCLUSIVE)。这就解释了为什么
jstack输出中:synchronized锁等待显示BLOCKED,而ReentrantLock锁等待显示WAITING on java.util.concurrent.locks...。
三、生产避坑
3.1 线程假死问题
场景:线上服务突然无响应,jstack 发现大量线程处于 WAITING 状态,但 notify() 早已被调用,线程却始终不唤醒。
根因:使用了 notify() 而不是 notifyAll()。由于等待同一条件的线程可能被放入不同的等待队列(取决于 JVM 实现细节),notify() 只唤醒其中一个,导致其他线程永远等待。
正确做法:除非你 100% 确定只有一条线程在等待,否则使用 notifyAll()。代价是惊群效应(多个线程同时被唤醒竞争锁),但正确性优先于性能。
3.2 线程池饥饿死锁
场景:使用 Executors.newFixedThreadPool(1) 单线程池,在任务中调用 submit() 提交另一个任务等待同一线程池执行。由于池中唯一线程正在等待自己,产生线程池内部死锁。
排查:jstack 中看到两条线程互相 WAITING on java.util.concurrent.FutureTask,形成循环依赖。
面试加分点:能说出"Java 16 引入了虚线程(Virtual Threads),虚线程在 LockSupport.park() 时不会阻塞底层 OS 线程,而是将虚线程挂起到堆中的 Continuation 对象上,彻底解决了'百万线程'的内存问题"。这说明你有 JDK 最新特性的跟进。
面试陷阱:被问到"线程状态和线程优先级"时,很多人会说"BLOCKED 线程优先级更高"。这是错误的。线程优先级只是对 OS 调度器的建议,不保证任何顺序。BLOCKED 和 WAITING 的唤醒顺序取决于具体的同步机制(synchronized 的 Entry Set 是 FIFO?AQS 的等待队列是 FIFO?——实际上 JDK 8 的 synchronized 不保证 FIFO,但 AQS 是 CLH 队列严格 FIFO)。