欢迎光临
我们一直在努力

【大白话说Java面试题 第121题】【并发篇】第21题:wait 和 sleep 的区别?

📌 人工智能开发:AI Agents 开发实践

第21题:wait 和 sleep 的区别?

📚 回答:

  • 核心考点: wait() 和 sleep() 是 Java 线程控制中最基础也最容易混淆的两个方法。大厂面试不会只问"方法归属、唤醒时机、锁特性"这种表面区别,而是深入考察 底层实现原理(Monitor 等待队列 vs 线程调度器)、CPU 资源占用差异(WAITING vs TIMED_WAITING 状态)、虚假唤醒的防御、中断响应的差异,以及 为什么 wait() 必须在同步块内调用而 sleep() 不需要。面试官真正想判断的是:你是否建立了从 API 到底层调度器的完整认知链路。

1. 六大维度全面对比
对比维度wait() / wait(long)sleep(long)面试踩坑点
方法归属 Object 实例方法 Thread 静态方法 wait 不是 Thread 的方法!
调用前提 必须持有对象锁(同步块内) 无限制,任意位置调用 无锁调用 wait 抛 IllegalMonitorStateException
锁行为 释放锁 不释放锁 sleep 在同步块内会阻塞其他线程
唤醒方式 notify() / notifyAll() / 中断 / 超时 超时自动恢复 / 中断 wait 可被提前唤醒,sleep 只能等时间到
线程状态 WAITING / TIMED_WAITING TIMED_WAITING 两者状态不同,dump 线程时注意区分
CPU 占用 不占用 CPU,进入等待队列 不占用 CPU,进入休眠队列 两者都不消耗 CPU,但调度机制不同
使用场景 线程间协作(生产者-消费者) 延时执行、定时任务 用 sleep 做线程协作是典型错误

2. 底层实现原理深度解析
  • 2.1 wait() 的底层实现——Monitor 等待队列

wait() 的底层依赖 JVM 的 ObjectMonitor(C++ 实现),核心数据结构:

// HotSpot ObjectMonitor 核心字段(简化)
ObjectMonitor() {
_header = NULL; // 对象头 Mark Word
_count = 0; // 记录重入次数
_waiters = 0; // 等待线程数
_recursions = 0; // 锁重入次数
_object = NULL; // 关联对象
_owner = NULL; // 持有锁的线程
_WaitSet = NULL; // ★ 等待队列(Wait Set)
_EntryList = NULL; // 阻塞队列(Entry List)
}

wait() 执行流程:

1. 检查当前线程是否持有 _owner(即对象锁)
→ 未持有 → 抛 IllegalMonitorStateException
→ 持有 → 继续

2. 将当前线程封装成 ObjectWaiter 节点

3. 释放锁(_owner = NULL,_recursions = 0)

4. 将线程加入 _WaitSet 队列(等待队列)

5. 线程状态变为 WAITING / TIMED_WAITING

6. 挂起线程(park),等待被唤醒

7. 被 notify() / 中断 / 超时唤醒后:
→ 从 _WaitSet 移除
→ 重新竞争锁(加入 _EntryList 或自旋)
→ 获取锁后从 wait() 返回

关键认知:wait() 释放锁是为了让其他线程能够获取锁并执行 notify(),否则将发生死锁。[citation:0]

  • 2.2 sleep() 的底层实现——线程调度器

sleep() 的底层不依赖对象锁,而是直接操作线程调度器:

// OpenJDK 底层实现(简化)
JVM_Sleep(JNIEnv *env, jclass threadClass, jlong millis) {
// 1. 检查是否中断
if (Thread::is_interrupted(thread, true)) {
throw new InterruptedException();
}

// 2. 计算绝对唤醒时间
jlong prev_time = javaTimeNanos();

// 3. 挂起线程(不释放任何锁!)
thread->osthread()->set_state(MONITOR_WAIT);
os::sleep(thread, millis, false); // ★ 直接挂起,不操作 Monitor

// 4. 被唤醒后检查中断状态
if (Thread::is_interrupted(thread, true)) {
throw new InterruptedException();
}
}

sleep() 执行流程:

1. 检查中断状态(若已中断直接抛异常)

2. 记录当前时间,计算唤醒时间戳

3. 线程状态变为 TIMED_WAITING

4. 挂起线程(os::sleep),不释放任何锁

5. 时间到或被中断后恢复:
→ 若被中断,清除中断标志并抛 InterruptedException
→ 若时间到,正常返回

核心差异:sleep() 直接调用操作系统 API 挂起线程,全程不涉及 Monitor 对象,因此与锁无关。[citation:1]

  • 2.3 线程状态转换图

NEW
│
▼ start()
RUNNABLE
│
┌────────────┼────────────┐
▼ ▼ ▼
wait() sleep()/join() synchronized
无超时 有超时 竞争失败
│ │ │
▼ ▼ ▼
WAITING TIMED_WAITING BLOCKED
│ │ │
│ notify() │ 超时/中断 │ 获取锁
│ 中断 ▼ ▼
└──────→ RUNNABLE ←─────┘
│
▼ run()结束
TERMINATED

注意:wait() 超时版本(wait(long))进入的是 TIMED_WAITING,无参版本进入 WAITING;sleep() 始终进入 TIMED_WAITING。[citation:2]


3. 为什么 wait() 必须在同步块内调用?

这是面试中最经典的追问,需要从 设计语义 和 实现安全 两个层面回答:

  • 3.1 设计语义层面

wait()/notify() 的设计目的是 线程间协作,典型场景是生产者-消费者模式:

// 生产者
synchronized (queue) {
while (queue.isFull()) {
queue.wait(); // 队列满了,等待消费者消费
}
queue.add(item);
queue.notifyAll(); // 通知等待的消费者
}

// 消费者
synchronized (queue) {
while (queue.isEmpty()) {
queue.wait(); // 队列空了,等待生产者生产
}
item = queue.remove();
queue.notifyAll(); // 通知等待的生产者
}

协作的前提:检查条件(queue.isFull())和修改条件(queue.add())必须是原子的。如果 wait() 不在同步块内,可能出现:

// ❌ 错误:无锁保护
if (queue.isEmpty()) { // T1 检查:为空
queue.wait(); // T2 插入数据并 notify()(在 T1 wait 之前!)
} // T1 永远等不到 notify,死锁!
item = queue.remove();

synchronized 确保 “检查-等待” 和 “修改-通知” 的原子性,避免竞态条件。[citation:3]

  • 3.2 实现安全层面

从 ObjectMonitor 源码看,wait() 的第一步就是检查 _owner:

// ObjectMonitor::wait() 源码(简化)
void ObjectMonitor::wait(TRAPS) {
Thread * const Self = THREAD;

// ★ 第一步:检查当前线程是否持有锁
if (_owner != Self) {
// 未持有锁 → 抛异常
THROW(vmSymbols::java_lang_IllegalMonitorStateException());
}

// … 后续:释放锁、加入 WaitSet、挂起
}

无锁调用 wait() 会直接抛 IllegalMonitorStateException,这是 JVM 的强制安全检查。[citation:4]


4. 中断响应的差异
特性wait()sleep()
中断时行为 抛出 InterruptedException,不清除中断标志 抛出 InterruptedException,清除中断标志
中断后状态 线程状态变为 RUNNABLE,需重新竞争锁 线程状态变为 RUNNABLE,直接继续执行
代码示例 catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (InterruptedException e) { /* 中断标志已被清除 */ }

关键差异:wait() 被中断后,中断标志位 仍然保留(JDK 文档明确说明);sleep() 被中断后,中断标志位 被清除。

// wait() 中断处理
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
// wait() 不清除中断标志,但建议恢复
Thread.currentThread().interrupt();
}
}

// sleep() 中断处理
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
// sleep() 已清除中断标志,无需再恢复
// 如需保留,需手动:Thread.currentThread().interrupt();
}


5. 虚假唤醒与防御
  • 5.1 什么是虚假唤醒?

JVM 允许 wait() 在没有 notify() 的情况下被唤醒(由操作系统或 JVM 内部机制触发)。虽然概率极低,但在高并发下可能发生。

  • 5.2 为什么 sleep() 没有虚假唤醒?

sleep() 基于定时器调度,时间到必然唤醒,不存在虚假唤醒问题。但 sleep() 无法被 notify() 提前唤醒。

  • 5.3 防御代码对比

// wait() 必须用 while 防御虚假唤醒
synchronized (lock) {
while (conditionNotMet) { // ✅ while 循环
lock.wait();
}
// 执行业务逻辑
}

// sleep() 无需防御虚假唤醒,但无法响应业务条件
Thread.sleep(1000); // 时间到自动醒,无需循环检查


6. 生产环境避坑指南
  • 6.1 不要在同步块内用 sleep() 代替 wait()

// ❌ 错误:用 sleep 做线程协作
synchronized (queue) {
while (queue.isEmpty()) {
Thread.sleep(100); // 不释放锁!其他线程无法操作 queue
}
}

// ✅ 正确:用 wait 释放锁
synchronized (queue) {
while (queue.isEmpty()) {
queue.wait(); // 释放锁,让其他线程可以操作 queue
}
}

后果:sleep() 不释放锁,会导致其他线程长期无法获取锁,系统吞吐量暴跌。

  • 6.2 wait() 必须在循环内调用

// ❌ 错误:用 if 检查条件
synchronized (lock) {
if (conditionNotMet) { // 虚假唤醒后不会重新检查!
lock.wait();
}
}

// ✅ 正确:用 while 检查条件
synchronized (lock) {
while (conditionNotMet) { // 唤醒后重新检查
lock.wait();
}
}

  • 6.3 中断处理必须恢复标志位

// ❌ 错误:吞掉中断
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace(); // 中断标志被清除,上层无法感知
}

// ✅ 正确:恢复中断标志位
try {
lock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 重新设置中断标志
break; // 或 return,优雅退出
}

  • 6.4 notify() vs notifyAll() 的选择

// 单消费者场景:notify() 足够
synchronized (queue) {
queue.add(item);
queue.notify(); // 只唤醒一个消费者
}

// 多消费者/多生产者场景:必须用 notifyAll()
synchronized (queue) {
queue.add(item);
queue.notifyAll(); // 唤醒所有等待线程,各自检查条件
}

注意:notify() 随机唤醒一个线程,如果唤醒的是同类线程(如生产者唤醒生产者),可能导致所有线程都在等待的死锁(“假死”)。

  • 6.5 sleep(0) 的特殊含义

Thread.sleep(0); // 让出当前 CPU 时间片,进入就绪队列

sleep(0) 不是"不 sleep",而是立即触发线程调度,让同优先级或更高优先级的线程有机会执行。这与 yield() 类似,但 yield() 不进入 TIMED_WAITING 状态。

  • 6.6 wait() 和 sleep() 的精度问题

两者都依赖操作系统调度器,实际休眠时间 ≥ 指定时间(受 CPU 调度策略影响)。如果需要高精度定时,使用 java.util.concurrent 包下的 ScheduledExecutorService 或 LockSupport.parkNanos()。


7. 面试官追问与高分回答模板
  • 追问 1:“wait() 和 sleep() 有什么区别?”

低分回答:“wait() 是 Object 的方法,会释放锁;sleep() 是 Thread 的方法,不释放锁。”(太浅,没有触及底层)

高分回答:

"两者的区别要从 方法归属、锁行为、底层实现、使用场景 四个维度分析:

  • 方法归属:wait() 是 Object 的实例方法,每个对象都有;sleep() 是 Thread 的静态方法,只能作用于当前线程。
  • 锁行为:wait() 必须在同步块内调用,调用后释放对象锁;sleep() 可以在任意位置调用,不释放任何锁。
  • 底层实现:wait() 依赖 JVM 的 ObjectMonitor,将线程加入 _WaitSet 等待队列;sleep() 直接调用操作系统 API os::sleep,不涉及 Monitor。
  • 使用场景:wait() 用于线程间协作(生产者-消费者),sleep() 用于延时执行(定时任务、节流)。
  • 核心记忆:wait() 是’等别人通知我’,sleep() 是’我自己睡一会’。

    • 追问 2:“为什么 wait() 必须在同步块内调用?”

    低分回答:“因为 wait() 会释放锁,必须先持有锁。”(没有解释设计原因)

    高分回答:

    "有两个层面的原因:

    设计语义层面:wait()/notify() 用于线程间协作,协作的前提是’检查条件’和’修改条件’必须是原子的。如果不在同步块内,可能出现:线程 A 检查条件为空 → 线程 B 插入数据并 notify() → 线程 A 才调用 wait(),永远等不到通知,死锁。

    实现安全层面:ObjectMonitor 的 wait() 源码第一步就是检查 _owner 是否等于当前线程,未持有锁直接抛 IllegalMonitorStateException。这是 JVM 的强制安全检查。

    所以 synchronized 既是保护条件检查的原子性,也是 JVM 的安全前提。"

    • 追问 3:“wait() 释放锁后,被 notify() 唤醒时还能直接执行吗?”

    高分回答:

    "不能。wait() 被唤醒后需要 重新竞争锁,不是直接执行。

    具体流程:

  • 线程 A 调用 wait() → 释放锁 → 进入 _WaitSet。
  • 线程 B 获取锁 → 执行业务 → 调用 notify() → 释放锁。
  • 线程 A 从 _WaitSet 移出,进入 _EntryList 或自旋竞争锁。
  • 线程 A 竞争到锁后,才从 wait() 返回,继续执行。
  • 这也是为什么 wait() 必须在 while 循环内调用:从 wait() 返回到真正执行业务代码之间,条件可能被其他线程改变。"

    • 追问 4:“sleep() 会进入什么线程状态?和 wait() 的状态有什么区别?”

    高分回答:

    "sleep() 进入 TIMED_WAITING 状态;wait() 无参版本进入 WAITING,带超时版本进入 TIMED_WAITING。

    关键区别:

    • WAITING:无限期等待,必须被 notify() 或中断唤醒。
    • TIMED_WAITING:限期等待,超时后自动唤醒。
    • BLOCKED:等待获取锁,与 wait() 无关。

    从线程 dump 中可以看到:

    • sleep() 的线程状态:java.lang.Thread.State: TIMED_WAITING (sleeping)
    • wait() 的线程状态:java.lang.Thread.State: WAITING (on object monitor) 或 TIMED_WAITING (on object monitor)

    注意:BLOCKED 是等待锁(synchronized 竞争失败),WAITING/TIMED_WAITING 是主动等待(wait()/sleep()/join()),两者完全不同。"

    • 追问 5:“如果线程在 sleep() 时持有锁,其他线程能获取这个锁吗?”

    高分回答:

    "不能。sleep() 不释放任何锁,包括 synchronized 锁、ReentrantLock 锁等。

    示例:

    synchronized (lock) {
    Thread.sleep(10000); // 持有锁 10 秒,其他线程无法进入
    }

    这会导致严重的性能问题:其他需要该锁的线程全部阻塞。如果需要在等待时释放锁,必须使用 wait()(释放 Object 锁)或 Condition.await()(释放 ReentrantLock)。

    这也是 sleep() 不能用于线程协作的根本原因:它不释放锁,无法让其他线程修改共享条件。"

    • 追问 6:“wait() 和 sleep() 被中断时有什么区别?”

    高分回答:

    "两者都会抛出 InterruptedException,但中断标志位的处理不同:

  • sleep():被中断后,清除中断标志位(置为 false)。如果想保留中断状态,需要手动 Thread.currentThread().interrupt()。
  • wait():被中断后,不清除中断标志位(JDK 文档明确说明)。但最佳实践仍建议手动恢复,保持代码一致性。
  • 此外,中断后的行为也不同:

    • sleep() 被中断:立即抛出异常,线程变为 RUNNABLE,继续执行后续代码。
    • wait() 被中断:立即抛出异常,但线程需要 重新竞争锁 后才能从 wait() 返回,竞争到锁前仍处于阻塞状态。"

    8. 方案选型速查表
    业务场景推荐方法核心理由
    生产者-消费者协作 wait() / notify() 释放锁,允许其他线程修改条件
    延时执行、定时轮询 sleep() 简单直接,无需锁
    等待某个条件成立 wait() + while 循环 可响应条件变化,避免无效轮询
    固定间隔的周期性任务 ScheduledExecutorService 比 sleep() 更精准,支持异常处理
    需要超时控制的阻塞 wait(long) / awaitNanos() 超时自动唤醒,避免永久阻塞
    不持有锁时的线程暂停 sleep() / LockSupport.park() 无需锁,不会抛异常

    💡 面试官想要的满分总结:

    wait() 和 sleep() 的本质区别不是"方法归属",而是 设计目的 和 底层机制:

    wait() 是 线程协作 的工具,底层依赖 ObjectMonitor 的 _WaitSet 队列,调用时必须持有锁且会释放锁,被唤醒后需重新竞争锁。它解决的是"我等待某个条件,条件满足后别人通知我"的问题。

    sleep() 是 线程调度 的工具,底层直接调用操作系统 os::sleep,与锁无关,不释放锁,时间到自动恢复。它解决的是"我自己暂停一段时间"的问题。

    生产环境中的三个铁律:

  • 用 wait() 做协作,用 sleep() 做延时——两者绝不混用。
  • wait() 必须在 while 循环内调用——防御虚假唤醒。
  • 中断异常必须恢复标志位——Thread.currentThread().interrupt(),让上层代码感知中断。
  • 真正理解这两个方法,需要深入到 HotSpot 的 ObjectMonitor 源码和线程状态转换模型。面试中能讲清楚 _WaitSet、_EntryList、WAITING vs TIMED_WAITING,就已经超越了 90% 的候选人。


    觉得对您有帮助,麻烦点点关注啦,您的关注是我创作的最大动力~ 🎯

    赞(0)
    未经允许不得转载:171主机测评 » 【大白话说Java面试题 第121题】【并发篇】第21题:wait 和 sleep 的区别?
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址