在 Java 并发编程的面试与实战中,线程生命周期是一个高频考点。很多开发者能背诵出 NEW、RUNNABLE 等状态,但在面对“线程挂起消耗什么资源”、“BLOCKED 和 WAITING 有什么区别”等深层问题时往往语焉不详。
本文将从 状态定义、CPU 交互、资源消耗 以及 内存分配 四个维度,彻底拆解 Java 线程的生命周期。
一、 Java 线程的 6 种状态全景图
Java 线程的生命周期由 java.lang.Thread.State 枚举定义。理解这些状态的关键在于区分 JVM 视角 与 操作系统视角。
1. 状态详解
| 状态 | 英文标识 | 核心特征 | CPU 占用 |
|---|---|---|---|
| 新建 | NEW | 线程对象已创建,但未调用 start()。 | 无 |
| 可运行 | RUNNABLE | 包含 OS 层面的 Ready(等待调度)和 Running(正在执行)。 | 可能占用 |
| 阻塞 | BLOCKED | 等待获取 synchronized 监视器锁。 | 无 |
| 无限期等待 | WAITING | 等待其他线程显式通知(如 notify)。 | 无 |
| 超时等待 | TIMED_WAITING | 等待特定时间或通知(如 sleep)。 | 无 |
| 终止 | TERMINATED | 线程执行结束或异常退出。 | 无 |
2. 状态转换流程图
stateDiagram-v2
direction TB
NEW --> RUNNABLE : start()
RUNNABLE --> BLOCKED : 竞争 synchronized 锁失败
BLOCKED --> RUNNABLE : 获取到锁
RUNNABLE --> WAITING : wait() / join() / park()
WAITING --> BLOCKED : 被 notify() 唤醒,重新竞争锁
BLOCKED --> RUNNABLE : 获取到锁
RUNNABLE --> TIMED_WAITING : sleep() / wait(t) / join(t)
TIMED_WAITING --> RUNNABLE : 超时 / 被通知 / 中断
RUNNABLE --> TERMINATED : run() 执行完毕
注意:从
WAITING被唤醒后,线程通常不会直接变为RUNNABLE,而是先进入BLOCKED状态去竞争锁,获取锁成功后才转为RUNNABLE。
二、 线程状态与 CPU 的真实关系
很多初学者误以为 RUNNABLE 就是正在占用 CPU,其实不然。
1. 唯一可能占用 CPU 的状态:RUNNABLE
Java 中的 RUNNABLE 是一个复合状态:
- Running:线程获得了 CPU 时间片,正在执行指令。
- Ready:线程具备执行资格,正在排队等待 CPU 调度。
结论:只有处于 RUNNABLE 且被操作系统调度器选中的线程,才会真正消耗 CPU 计算能力。其他所有状态(BLOCKED, WAITING 等)均不占用 CPU 时间片 。
2. 为什么需要非运行状态?
如果所有线程都处于 RUNNABLE,CPU 将在频繁的上下文切换中耗尽性能。通过让线程进入 WAITING 或 BLOCKED 状态,可以主动让出 CPU,使系统能够高效处理更多并发任务。
三、 “挂起”的线程到底在干什么?
日常所说的线程“挂起”,在 Java 中通常对应 BLOCKED、WAITING 或 TIMED_WAITING。它们虽然都不跑代码,但等待的资源完全不同。
1. BLOCKED:在门口“抢钥匙”
- 场景:多个线程竞争同一个
synchronized锁。 - 行为:线程位于对象监视器的 Entry List(入口队列)中。
- 恢复:当持有锁的线程释放锁,JVM 从 Entry List 中唤醒一个线程去竞争锁 。
2. WAITING:在屋里“睡觉等叫”
- 场景:调用
Object.wait()或LockSupport.park()。 - 行为:线程位于对象监视器的 Wait Set(等待集)中。关键点:进入此状态前,线程会释放已持有的锁 。
- 恢复:必须由其他线程调用
notify()/unpark()显式唤醒 。
3. TIMED_WAITING:定闹钟“睡觉”
- 场景:调用
Thread.sleep()或wait(timeout)。 - 行为:线程休眠指定时间。
sleep():不释放锁,仅暂停执行。wait(timeout):释放锁,进入等待集并启动计时器 。
- 恢复:时间到期自动醒来,或被提前通知。
四、 非运行状态线程消耗什么资源?
这是一个常见的误区:线程不跑代码,就不消耗资源吗?
答案是:不消耗 CPU,但严重消耗内存!
1. 内存消耗(主要开销)
只要线程未终止(TERMINATED),无论其处于何种状态,以下内存都不会释放:
- 虚拟机栈(Stack):每个线程默认分配约 1MB 的栈空间(可通过
-Xss调整)。这是线程最大的内存开销,用于存储局部变量、方法调用链等 。 - 原生线程结构:操作系统内核为每个线程维护的控制块(如 Linux 的
task_struct),属于堆外内存。 - Thread 对象:堆内存中的
java.lang.Thread实例。
风险:如果系统中存在成千上万个处于 WAITING 状态的线程,即使它们不占 CPU,也可能因耗尽本地内存导致 OutOfMemoryError: unable to create new native thread 。
2. 上下文切换开销
当大量线程在 BLOCKED 和 RUNNABLE 之间频繁切换时,会导致 CPU 缓存失效,增加调度器的负担,从而降低系统整体吞吐量 。
五、 线程内存分配在哪里?
理解线程内存分布有助于排查 OOM 问题。
| 内存区域 | 分配位置 | 是否受 GC 管理 | 说明 |
|---|---|---|---|
| 线程栈 (Stack) | 堆外 (Native) | 否 | 每个线程独占,存储栈帧。OOM 常见源头 。 |
| Thread 对象 | 堆内 (Heap) | 是 | 存放线程元数据(名称、状态等)。 |
| TLAB | 堆内 (Heap) | 是 | Eden 区中线程私有的对象分配缓冲区,加速对象创建 。 |
| 程序计数器 | 堆外 (Native) | 否 | 极小,记录当前执行的字节码行号。 |
六、 总结与最佳实践
- 状态本质:
BLOCKED是被动等待锁。WAITING是主动让权等通知。TIMED_WAITING是带超时的等待。
- 资源视角:线程是“CPU 友好”但“内存敏感”的资源。非运行状态线程不占 CPU,但持续占用栈内存。
- 开发建议:
- 使用线程池:严格限制最大线程数,避免无限创建线程导致内存溢出。
- 减少锁竞争:优化同步代码块,减少线程进入
BLOCKED状态的频率。 - 合理超时:在使用
wait或join时,尽量设置超时时间,防止线程永久挂起。
评论区