目录
一、线程的五种基本状态
二、线程从 RUNNABLE 进入阻塞 / 等待状态的三种典型场景
1. 调用sleep(long millis):进入 TIMED_WAITING 状态
2. 调用wait():进入 WAITING/TIMED_WAITING 状态
3. 等待 I/O 资源或获取锁失败:进入 BLOCKED 状态
三、线程从阻塞 / 等待状态回到 RUNNABLE 状态的底层原理
1. TIMED_WAITING 状态的唤醒(如sleep()超时)
2. WAITING/TIMED_WAITING 状态的唤醒(如wait()被唤醒)
对象监视器(Monitor)
wait()方法的底层执行流程
notify()/notifyAll()的底层执行流程
3. BLOCKED 状态的唤醒(如获取锁或 I/O 就绪)
四、总结:线程状态转换的核心逻辑
在多线程编程中,线程状态的转换是理解并发行为的核心。当运行中的线程因调用sleep()
、wait()
或等待 I/O 等操作让出 CPU 时,会进入特定的阻塞状态;而从阻塞状态重新回到可运行状态的过程,更是体现了 JVM 对线程调度的精妙设计。本文将深入解析线程状态转换的底层机制,揭示线程如何在不同状态间切换。
一、线程的五种基本状态
根据 Java 线程模型,线程的生命周期包含五种状态,由Thread.State
枚举定义:
- 新建(NEW):线程对象已创建但未调用
start()
方法。 - 可运行(RUNNABLE):线程已启动,正在 JVM 中运行或等待 CPU 调度。
- 阻塞(BLOCKED):线程等待获取 synchronized 锁(进入 synchronized 块 / 方法时未抢到锁)。
- 等待(WAITING):线程无限期等待被其他线程唤醒(如调用
wait()
、join()
无参方法)。 - 超时等待(TIMED_WAITING):线程在指定时间内等待(如
sleep(1000)
、wait(1000)
)。 - 终止(TERMINATED):线程执行完毕或因异常退出。
核心转换场景:运行状态(RUNNABLE)的线程会因特定操作进入阻塞 / 等待状态,待条件满足后重新回到 RUNNABLE 状态。
二、线程从 RUNNABLE 进入阻塞 / 等待状态的三种典型场景
运行中的线程(RUNNABLE)在以下情况会让出 CPU,进入非运行状态:
1. 调用sleep(long millis)
:进入 TIMED_WAITING 状态
sleep()
是 Thread 类的静态方法,作用是让当前线程暂停执行指定毫秒数。调用后线程状态变化为:
RUNNABLE → TIMED_WAITING
- 特点:
- 线程不会释放已持有的锁(如 synchronized 锁)。
- 暂停时间是相对精确的(受系统时钟和 CPU 调度影响)。
- 常用于模拟延迟(如网络请求等待)。
public class SleepDemo {public static void main(String[] args) throws InterruptedException {Thread t = new Thread(() -> {try {System.out.println("线程进入睡眠");Thread.sleep(3000); // 进入TIMED_WAITING状态System.out.println("线程唤醒");} catch (InterruptedException e) {e.printStackTrace();}});t.start();}
}
2. 调用wait()
:进入 WAITING/TIMED_WAITING 状态
wait()
是 Object 类的方法,必须在synchronized
块中调用,作用是让线程释放锁并等待被唤醒。调用后状态变化为:
RUNNABLE → WAITING(无参wait()
)或TIMED_WAITING(wait(long timeout)
)
- 特点:
- 必须持有对象锁(synchronized),调用后立即释放锁。
- 需其他线程调用同一对象的
notify()
/notifyAll()
唤醒(或超时自动唤醒)。 - 常用于线程间协作(如生产者消费者模型)。
public class WaitDemo {private static final Object lock = new Object();public static void main(String[] args) {new Thread(() -> {synchronized (lock) {try {System.out.println("线程进入等待");lock.wait(); // 进入WAITING状态System.out.println("线程被唤醒");} catch (InterruptedException e) {e.printStackTrace();}}}).start();}
}
3. 等待 I/O 资源或获取锁失败:进入 BLOCKED 状态
当线程等待外部资源(如磁盘 IO、网络请求)或尝试获取 synchronized 锁但失败时,会进入 BLOCKED 状态:
RUNNABLE → BLOCKED
- 特点:
- 等待 I/O 时,线程暂停执行,直到资源就绪(如数据读取完成)。
- 获取锁失败时,线程进入锁的 "入口集"(Entry Set)排队,直到锁被释放。
public class BlockedDemo {private static final Object lock = new Object();public static void main(String[] args) {// 线程1先获取锁new Thread(() -> {synchronized (lock) {try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); }}}).start();// 线程2后启动,尝试获取锁失败,进入BLOCKED状态new Thread(() -> {synchronized (lock) { // 此处会阻塞System.out.println("线程2获取到锁");}}).start();}
}
三、线程从阻塞 / 等待状态回到 RUNNABLE 状态的底层原理
线程进入阻塞 / 等待状态后,并非永久停止,而是在特定条件满足后重新回到 RUNNABLE 状态。不同状态的唤醒机制底层原理不同:
1. TIMED_WAITING 状态的唤醒(如sleep()
超时)
sleep(long millis)
的唤醒依赖系统定时器:
-
底层流程:
- 线程调用
sleep()
时,JVM 向操作系统注册一个定时事件(指定 millis 后触发)。 - 线程从 RUNNABLE 状态切换到 TIMED_WAITING,操作系统将其从 CPU 调度队列中移除。
- 定时时间到达后,操作系统触发事件,JVM 将线程重新放入 CPU 调度队列,状态切换为 RUNNABLE。
- 线程等待 CPU 调度,获取时间片后继续执行。
- 线程调用
-
特殊情况:若线程在
sleep()
期间被中断(调用interrupt()
),会抛出InterruptedException
并提前唤醒,状态直接回到 RUNNABLE。
2. WAITING/TIMED_WAITING 状态的唤醒(如wait()
被唤醒)
要理解wait()
和notify()
/notifyAll()
的底层原理,需要从线程状态转换和对象监视器(Monitor) 两个核心角度展开。这组方法是 Java 中实现线程间协作的基础,其底层依赖于 JVM 对对象锁的管理机制。
对象监视器(Monitor)
Java 中每个对象都隐式关联一个对象监视器(Monitor),也称为 “内置锁” 或 “管程”。它是实现同步和线程协作的核心数据结构,内部包含三个关键部分:
- 持有线程:当前获取到锁的线程(同一时间只能有一个线程持有)。
- 入口集(Entry Set):等待获取锁的线程集合(线程状态为
BLOCKED
)。 - 等待集(Wait Set):调用
wait()
后释放锁并等待被唤醒的线程集合(线程状态为WAITING
或TIMED_WAITING
)。
简单说,Monitor 就像一个 “休息室 + 会议室”:
- 会议室(锁)同一时间只能有一个线程使用(持有锁的线程);
- 入口集是等待进入会议室的线程队列;
- 等待集是在会议室内主动暂停(调用
wait()
)并临时退出到休息室的线程队列。
wait()
方法的底层执行流程
当线程调用obj.wait()
时,底层会执行以下操作(必须在synchronized
块中调用,否则会抛IllegalMonitorStateException
):
-
释放当前对象的锁
调用wait()
的线程必须已经持有obj
的锁(即处于synchronized(obj)
块中)。执行wait()
后,线程会主动释放该锁,退出 “持有线程” 位置,让其他线程有机会获取锁。 -
进入对象的等待集(Wait Set)
线程释放锁后,会从 “运行状态” 转入 “等待状态(WAITING)”,并被放入obj
的等待集(休息室)中。此时线程不再参与 CPU 调度,处于阻塞状态。 -
等待被唤醒
线程在等待集中休眠,直到其他线程调用obj.notify()
或obj.notifyAll()
,才有可能被唤醒。若调用的是wait(long timeout)
,则超时后也会自动唤醒。 -
重新竞争锁
被唤醒的线程不会立即执行,而是从等待集转移到入口集(Entry Set),重新参与锁的竞争。只有重新获取到obj
的锁后,才能从wait()
方法返回,继续执行后续代码。
-
关键细节:被唤醒的线程必须重新竞争锁,因此
wait()
通常配合while
循环检查条件(防止虚假唤醒):synchronized (obj) {while (条件不满足) { // 用while而非if,避免虚假唤醒obj.wait();} }
notify()
/notifyAll()
的底层执行流程
notify()
和notifyAll()
用于唤醒等待集中的线程,底层流程如下:
notify()
:从对象的等待集中随机选择一个线程,将其从等待集移到入口集(Entry Set),使其有机会重新竞争锁。notifyAll()
:将对象等待集中的所有线程全部移到入口集,让所有等待线程重新竞争锁。
注意:
- 调用
notify()
/notifyAll()
的线程不会立即释放锁,而是要等当前synchronized
块执行完毕后才释放。因此,被唤醒的线程需要等待唤醒它的线程释放锁后,才能重新竞争锁。 - 若等待集中没有线程,
notify()
/notifyAll()
调用无效果。
3. BLOCKED 状态的唤醒(如获取锁或 I/O 就绪)
-
获取锁的 BLOCKED 线程:
- 线程因未抢到 synchronized 锁进入 BLOCKED 状态,排队在锁的入口集(Entry Set)。
- 当持有锁的线程释放锁(退出 synchronized 块),JVM 从入口集中唤醒一个或多个线程(具体策略由 JVM 实现决定)。
- 被唤醒的线程竞争锁,成功后状态切换为 RUNNABLE。
-
等待 I/O 的 BLOCKED 线程:
- 线程发起 I/O 请求后,进入 BLOCKED 状态(如读取文件时等待磁盘数据)。
- 操作系统负责处理 I/O 操作,完成后向 JVM 发送I/O 完成事件。
- JVM 接收到事件后,将线程状态切换为 RUNNABLE,等待 CPU 调度。
四、总结:线程状态转换的核心逻辑
线程状态转换的本质是JVM 与操作系统协作的资源调度过程:
- 主动让出 CPU:
sleep()
、wait()
等方法让线程暂时放弃执行权,进入特定状态。 - 阻塞原因解除:超时、被唤醒、资源就绪等条件触发时,线程重新获得执行资格。
- 竞争 CPU 时间片:所有回到 RUNNABLE 状态的线程,最终需通过操作系统的 CPU 调度获得执行机会。