Java 中 wait () 与 sleep () 的核心区别
一、前置知识的深度补充:线程状态转换图
原文提到了线程状态,这里补充完整的线程状态转换图,帮助理解wait()和sleep()在状态机中的位置。
text
┌─────────────────────────────────────┐
│ NEW(新建) │
│ 线程对象创建,未启动 │
└─────────────────┬───────────────────┘
│ start()
↓
┌─────────────────────────────────────┐
│ RUNNABLE(就绪/运行) │
│ 等待CPU时间片 / 正在执行 │
└───┬─────────────┬───────────────┬───┘
│ │ │
获得锁失败 │ sleep() │ wait() │ 锁竞争成功
↓ ↓ ↓
┌───────────────────┐ ┌───────────────┐ ┌───────────────┐
│ BLOCKED(阻塞) │ │TIMED_WAITING │ │ WAITING │
│ 等待获取对象锁 │ │ 计时等待 │ │ 无限等待 │
└───────────────────┘ │ sleep(time) │ │ wait() │
│ wait(time) │ │ park() │
└───────┬───────┘ └───────┬───────┘
│ │
超时/唤醒 notify()
↓ ↓
┌─────────────────────────────────────┐
│ RUNNABLE │
└─────────────────────────────────────┘
关键点:
-
sleep()进入的是TIMED_WAITING,时间到自动恢复 -
wait()无参进入的是WAITING,必须有notify()唤醒 -
wait(timeout)进入的是TIMED_WAITING,既可以被唤醒也可以超时恢复
二、wait()的底层原理深度解析
2.1 wait()的完整执行流程
当线程调用lock.wait()时,JVM执行以下步骤:
cpp
// ObjectMonitor::wait() 简化版伪代码
void ObjectMonitor::wait(Thread* self) {
// 1. 检查当前线程是否持有锁
if (_owner != self) {
throw IllegalMonitorStateException();
}
// 2. 将当前线程加入_WaitSet(等待队列)
ObjectWaiter node(self);
_WaitSet = &node;
// 3. 释放锁(清空_owner,重入次数清零)
_recursions = 0;
_owner = NULL;
// 4. 唤醒_EntryList中的线程竞争锁
// 5. 阻塞当前线程(进入WAITING状态)
self->park();
// 6. 被唤醒后,重新竞争锁
// 7. 重新获取锁后,恢复执行
}
2.2 _WaitSet vs _EntryList 的区别
| 队列 | 作用 | 进入条件 | 唤醒方式 |
|---|---|---|---|
| _EntryList | 等待获取锁的线程 | 尝试获取锁失败 | 锁释放后竞争 |
| _WaitSet | 调用wait()的线程 | 已持有锁,调用wait() | notify()/notifyAll() |
关键理解:
-
_EntryList中的线程是在等锁 -
_WaitSet中的线程是在等条件
2.3 为什么wait()必须在synchronized中?
从源码可以看出:
-
wait()需要检查当前线程是否持有锁(_owner != self) -
需要操作
_WaitSet,这涉及Monitor内部数据结构 -
需要释放锁,这涉及
_owner和_recursions的修改
如果不在synchronized中调用:
-
当前线程没有持有Monitor,
_owner不是当前线程 -
抛出
IllegalMonitorStateException
三、sleep()的底层原理深度解析
3.1 sleep()的底层实现
sleep()是Thread类的静态方法,底层调用操作系统提供的定时器功能:
cpp
// Thread.sleep() 底层实现(简化)
void Thread::sleep(jlong millis) {
// 1. 设置定时器
// 2. 将线程状态从RUNNABLE改为TIMED_WAITING
// 3. 调用操作系统sleep函数(如Linux的nanosleep)
// 4. 时间到达后,操作系统将线程状态改回RUNNABLE
// 5. 等待CPU调度
}
3.2 sleep()与CPU时间片
sleep()期间:
-
线程不会占用CPU时间片
-
但不会释放任何锁
-
线程从运行队列移到等待队列
-
时间到后,线程重新进入就绪队列等待调度
3.3 sleep(0)的含义
sleep(0)不会让线程真正休眠,而是:
-
主动让出当前时间片
-
重新参与CPU竞争
-
相当于
Thread.yield()
使用场景:让出CPU给同优先级的其他线程执行机会。
四、wait()和sleep()的深层对比
4.1 锁行为的本质区别
| 维度 | wait() | sleep() |
|---|---|---|
| 释放Monitor | ✅ 完全释放 | ❌ 不释放 |
| 释放重入计数 | ✅ _recursions清零 |
❌ 保持不变 |
| 唤醒_EntryList | ✅ 唤醒等待锁的线程 | ❌ 不唤醒 |
| 重新竞争锁 | ✅ 唤醒后需重新竞争 | ❌ 锁一直在手 |
4.2 唤醒机制的底层区别
wait()唤醒流程:
text
其他线程调用notify()
↓
JVM从_WaitSet中取出一个线程
↓
将该线程移动到_EntryList
↓
当前线程释放锁后,该线程竞争锁
↓
竞争成功,恢复执行
sleep()唤醒流程:
text
操作系统定时器中断
↓
将线程从等待队列移到就绪队列
↓
等待CPU调度
↓
恢复执行(锁还在)
4.3 虚假唤醒(Spurious Wakeup)
这是wait()特有的问题,sleep()不存在。
什么是虚假唤醒?
线程在没有被notify()/notifyAll()的情况下,从wait()中唤醒。
原因:
-
操作系统层面的实现细节
-
某些JVM实现可能产生虚假唤醒
正确写法:
java
// 错误写法(可能产生问题)
synchronized (lock) {
if (!condition) {
lock.wait(); // 可能虚假唤醒
}
// 条件不满足时执行,出错
}
// 正确写法
synchronized (lock) {
while (!condition) { // 循环检查条件
lock.wait();
}
// 条件一定满足
}
五、wait/notify的生产者-消费者完整示例
5.1 标准实现
java
public class ProducerConsumerDemo {
private static final int CAPACITY = 10;
private final Queue<Integer> queue = new LinkedList<>();
private final Object lock = new Object();
// 生产者
public void produce() throws InterruptedException {
int value = 0;
while (true) {
synchronized (lock) {
// 队列满,等待
while (queue.size() == CAPACITY) {
lock.wait();
}
queue.offer(value);
System.out.println("生产: " + value++);
// 唤醒消费者
lock.notifyAll();
Thread.sleep(100); // 模拟生产耗时
}
}
}
// 消费者
public void consume() throws InterruptedException {
while (true) {
synchronized (lock) {
// 队列空,等待
while (queue.isEmpty()) {
lock.wait();
}
int value = queue.poll();
System.out.println("消费: " + value);
// 唤醒生产者
lock.notifyAll();
Thread.sleep(100); // 模拟消费耗时
}
}
}
}
5.2 为什么用notifyAll()而不是notify()?
| 方法 | 行为 | 风险 |
|---|---|---|
notify() |
唤醒_WaitSet中的一个随机线程 | 可能唤醒同类型线程,导致死锁 |
notifyAll() |
唤醒_WaitSet中的所有线程 | 性能稍差,但安全 |
典型死锁场景:
-
多个生产者、多个消费者
-
使用
notify()可能唤醒生产者,而消费者仍被阻塞 -
队列满时,所有生产者等待,没有消费者被唤醒 → 死锁
六、wait()和sleep()的异常处理
6.1 InterruptedException的处理
两者都会抛出InterruptedException,正确处理方式:
java
// 正确:传递中断状态
public void waitExample() throws InterruptedException {
synchronized (lock) {
lock.wait();
}
}
// 正确:恢复中断状态
public void sleepExample() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 恢复中断状态
Thread.currentThread().interrupt();
// 或者处理中断逻辑
}
}
// 错误:吞掉异常
public void badExample() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// 什么都不做,中断状态丢失
}
}
6.2 为什么Thread.sleep()需要处理异常?
sleep()期间,其他线程可以调用interrupt()中断它:
-
中断后,
sleep()立即抛出InterruptedException -
线程的中断状态被清除
-
必须正确处理,否则中断信号丢失
七、面试高频问题与深度回答
Q1:wait()和sleep()哪个性能更好?
答:没有绝对的优劣,取决于场景。
-
sleep():不释放锁,其他线程无法执行同步代码,适合不需要释放锁的延时场景 -
wait():释放锁,允许其他线程执行,适合需要协作的场景 -
如果需要长时间等待,
wait()释放锁能让系统更高效
Q2:wait()和sleep()都能用时间参数,有什么区别?
| 维度 | wait(timeout) | sleep(timeout) |
|---|---|---|
| 释放锁 | ✅ 释放 | ❌ 不释放 |
| 可被提前唤醒 | ✅ notify()/notifyAll() | ❌ 只有时间到 |
| 状态 | TIMED_WAITING | TIMED_WAITING |
| 唤醒后 | 需重新竞争锁 | 锁还在手 |
Q3:wait()的线程被notify()唤醒后,会立即执行吗?
答:不会立即执行。
-
被唤醒的线程从
_WaitSet移到_EntryList -
必须等待当前持有锁的线程释放锁
-
重新竞争锁成功后才能继续执行
Q4:可以在synchronized块外调用wait()吗?
答:不可以。会抛出IllegalMonitorStateException。因为:
-
wait()需要操作对象的Monitor -
只有在持有Monitor的情况下才能调用
Q5:Thread.sleep()和Object.wait()的底层实现差异?
| 维度 | sleep() | wait() |
|---|---|---|
| 实现层级 | Thread类的静态方法,调用OS定时器 | Object类的实例方法,操作ObjectMonitor |
| 涉及数据结构 | 线程控制块(TCB) | ObjectMonitor的_WaitSet |
| 是否涉及锁 | 不涉及 | 涉及Monitor的_owner |
| 唤醒机制 | OS定时中断 | notify()调用 |
八、实战避坑指南
8.1 常见错误1:在synchronized外调用wait()
java
// 错误
Object lock = new Object();
lock.wait(); // IllegalMonitorStateException
// 正确
synchronized (lock) {
lock.wait();
}
8.2 常见错误2:使用if而非while
java
// 错误
synchronized (lock) {
if (!condition) { // 可能虚假唤醒
lock.wait();
}
// 执行
}
// 正确
synchronized (lock) {
while (!condition) { // 循环检查
lock.wait();
}
// 执行
}
8.3 常见错误3:使用notify()导致线程饥饿
java
// 错误:使用notify()可能导致某些线程永远不被唤醒 lock.notify(); // 正确:使用notifyAll() lock.notifyAll();
8.4 常见错误4:sleep()中持有锁导致性能问题
java
// 错误:睡眠时持有锁,阻塞其他线程
synchronized (lock) {
Thread.sleep(5000); // 其他线程无法进入
}
// 正确:如果需要等待,使用wait()
synchronized (lock) {
lock.wait(5000); // 释放锁,其他线程可进入
}
九、总结:一图胜千言
text
┌─────────────────────────────────────────────────────────────────┐ │ wait() vs sleep() 全景对比 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 所属类: Object Thread │ │ ↓ ↓ │ │ 锁行为: 释放锁 不释放锁 │ │ ↓ ↓ │ │ 调用条件: synchronized内 任意位置 │ │ ↓ ↓ │ │ 线程状态: WAITING/TIMED_WAITING TIMED_WAITING │ │ ↓ ↓ │ │ 唤醒方式: notify()/notifyAll() 时间到 │ │ ↓ ↓ │ │ 使用场景: 线程通信/协作 线程延时 │ │ ↓ ↓ │ │ 典型应用: 生产者-消费者 定时任务 │ │ │ └─────────────────────────────────────────────────────────────────┘
核心结论:
-
本质不同:
wait()是对象锁机制,sleep()是线程调度机制 -
锁行为:
wait()释放锁,sleep()不释放锁——这是最本质的区别 -
调用条件:
wait()必须在synchronized中,sleep()无限制 -
唤醒方式:
wait()需要主动唤醒,sleep()自动苏醒 -
使用场景:线程协作用
wait()/notify(),线程延时用sleep()
一句话记住:
wait()是让出锁去等待,sleep()是抱着锁去睡觉。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)