一、前置知识的深度补充:线程状态转换图

原文提到了线程状态,这里补充完整的线程状态转换图,帮助理解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中?

从源码可以看出:

  1. wait()需要检查当前线程是否持有锁(_owner != self

  2. 需要操作_WaitSet,这涉及Monitor内部数据结构

  3. 需要释放锁,这涉及_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()唤醒后,会立即执行吗?

:不会立即执行。

  1. 被唤醒的线程从_WaitSet移到_EntryList

  2. 必须等待当前持有锁的线程释放锁

  3. 重新竞争锁成功后才能继续执行

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()             时间到            │
│                  ↓                              ↓               │
│  使用场景:   线程通信/协作                    线程延时           │
│                  ↓                              ↓               │
│  典型应用:   生产者-消费者                    定时任务           │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

核心结论

  1. 本质不同wait()是对象锁机制,sleep()是线程调度机制

  2. 锁行为wait()释放锁,sleep()不释放锁——这是最本质的区别

  3. 调用条件wait()必须在synchronized中,sleep()无限制

  4. 唤醒方式wait()需要主动唤醒,sleep()自动苏醒

  5. 使用场景:线程协作用wait()/notify(),线程延时用sleep()

一句话记住

wait()是让出锁去等待,sleep()是抱着锁去睡觉。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐