以下是一篇尽量通俗、结构清晰、图文并茂的文章,帮助你真正搞懂:
Java 线程的 6 种状态 + 状态转换图
以及 Java 内存模型(JMM)最核心的几件事
一、线程的 6 种状态(Thread.State 枚举)
Java 线程在 JVM 层面一共有这 6 种状态:
| 状态英文 | 中文常叫 | 是否占用 CPU | 是否持有锁 | 最通俗的比喻 |
|---|---|---|---|---|
| NEW | 新建 | 否 | 否 | 刚 new Thread(),相当于一张写好但还没投递的简历 |
| RUNNABLE | 可运行 / 运行中 | 是(或排队) | 可能有 | 线程已经“活了”,要么正在 CPU 上跑,要么在就绪队列排队等 CPU |
| BLOCKED | 锁阻塞 | 否 | 否 | 想进 synchronized 块,但发现门锁被别人拿着,像在银行排队等叫号 |
| WAITING | 无限期等待 | 否 | 否 | 主动把自己挂起(wait、join、park),像睡着了等别人叫醒 |
| TIMED_WAITING | 限时等待 | 否 | 否 | 带闹钟的等待(sleep、wait带时间、join带时间、parkNanos) |
| TERMINATED | 终止 / 死亡 | 否 | 否 | run() 执行完毕(正常结束或异常抛出未捕获),线程彻底凉了 |
最容易混淆的三兄弟对比
| 项目 | BLOCKED | WAITING | TIMED_WAITING |
|---|---|---|---|
| 在等待什么 | 等待 monitor 锁 | 等待别人主动唤醒 | 等待别人唤醒 或者 时间到 |
| 典型方法 | 进入 synchronized 块(没抢到锁) | wait() / join() / LockSupport.park() | sleep() / wait(时间) / join(时间) / parkNanos |
| 进入前是否持有锁 | 不持有(没抢到) | 持有 → 调用 wait 后释放 | 持有 → sleep 不释放,wait 释放 |
| 怎么被唤醒 | 锁被释放后自动去竞争 | notify / notifyAll / unpark | notify / unpark / 时间到 |
二、线程状态转换图(最经典的 7 条路径)
new Thread()
↓
NEW
↓ start()
┌───────────────┐
│ RUNNABLE │◄───────────────────────┐
│ (运行 / 就绪) │ │
└───────┬───────┘ │
│ │
CPU时间片用完 / yield() │
▼ │
┌───────────────┴───────────────┐ │
│ │ │
┌─────┴──────┐ ┌──────┴──────┐ │
│ BLOCKED │ │ WAITING │◄───────┼──┐
│ (锁等待) │ │ (无限等待) │ │ │
└─────┬──────┘ └──────┬──────┘ │ │
│ 锁释放后自动竞争 │ │ │
│ │ │ │
▼ ▼ │ │
┌───────────────┐ ┌───────────────┐ │ │
│ RUNNABLE │◄─────────── │ TIMED_WAITING │◄───────┘ │
└───────────────┘ │ (限时等待) │ │
└───────┬───────┘ │
│ │
notify / 时间到 / unpark │
▼ │
┌───────────────┐ │
│ TERMINATED │◄────────┘
│ (死亡) │
└───────────────┘
最常考的 7 条转换路径(一句话解释)
- NEW → RUNNABLE
调用start(),线程进入就绪队列(不一定立刻执行) - RUNNABLE → TERMINATED
run() 方法正常结束 或 抛出未捕获异常 - RUNNABLE → BLOCKED
想进入synchronized块,但锁被别人拿着 → 进入锁等待队列 - RUNNABLE → WAITING
obj.wait()(必须先持有锁,调用后释放锁)thread.join()(等目标线程死)LockSupport.park()
- RUNNABLE → TIMED_WAITING
Thread.sleep(毫秒)(不释放锁,最常用)obj.wait(毫秒)(释放锁)thread.join(毫秒)
- WAITING / TIMED_WAITING → RUNNABLE
- 被
notify/notifyAll唤醒(但不一定马上拿到锁) - 被
LockSupport.unpark()唤醒 - 等待时间到(自动醒来)
- BLOCKED → RUNNABLE
锁被释放后,JVM 从锁等待队列里挑一个线程去竞争锁(不保证公平)
三、Java 内存模型(JMM)最核心的几件事(白话版)
JMM 要解决的终极两个问题:
- 线程A改了变量,线程B什么时候能看见?(可见性)
- 我写的代码顺序,CPU真的会按这个顺序执行吗?(有序性)
1. 主内存 vs 每个线程的工作内存
主内存(所有线程共享)
age = 18 money = 1000
▲ ▲
┌──────────────────┬──────────────────┐
│ 线程A工作内存 │ 线程B工作内存 │
│ age副本 = 18 │ age副本 = 18 │
│ money副本 = 1000 │ money副本 = 1000 │
└──────────────────┴──────────────────┘
关键点:线程之间不能直接操作对方的内存,只能操作自己的工作内存副本。
2. happens-before 规则(最重要!)
只要满足下面任意一条,前面的写操作对后面的读操作是可见的:
- 程序顺序规则:单线程内,代码顺序就是执行顺序
- 监视器锁规则:解锁 → 同一个锁的加锁
- volatile 变量规则:对 volatile 变量的写 → 后面的读
- 线程启动规则:
start()→ 线程内任意操作 - 线程终止规则:线程内任意操作 →
join()返回 /isAlive()==false - 线程中断规则:
interrupt()→ 检测到中断(抛异常或isInterrupted()==true) - 对象终结规则:构造方法结束 →
finalize()开始 - 传递性:A 先于 B,B 先于 C → A 先于 C
最常考的三条白话版:
- 我对
volatile int x写了 100 → 后面任何线程读 x 一定是 100 - 我在
synchronized块里改了 age → 出块后别的线程进同一个锁的 synchronized 块一定能看到 - 我调用
t.start()→ t 线程里面的代码一定能看到start()之前的变量值
四、总结口诀(背下来就过关)
线程状态:
NEW → start → RUNNABLE ↔ {BLOCKED / WAITING / TIMED_WAITING} → TERMINATED
内存模型:
每个线程有自己的“草稿本”(工作内存)
想让别人看到我的修改,必须走“happens-before 桥梁”
三大桥梁:volatile、synchronized、start/join
如果你现在能:
- 画出线程状态转换图
- 说出 volatile 为什么能保证可见性
- 举出 3 个最常用的 happens-before 规则
那这篇内容你就已经掌握 80% 了。
需要再深入哪一块?
比如:
- volatile 底层(内存屏障 + Lock 前缀)
- synchronized 锁升级过程(偏向 → 轻量 → 重量)
- 三级缓存如何解决循环依赖
- ThreadLocal 内存泄漏原理
随时告诉我,我继续给你展开~