深入理解 sleep() 与 wait():从基础到监视器队列

前言

看似都是"让线程停下来",背后的原理却完全不同

在 Java 并发编程中,sleep() 和 wait() 是两个经常被拿来比较的方法。很多初学者甚至有一定经验的开发者,也容易混淆它们。今天这篇文章,我们就从基础区别 一路深入到监视器锁的队列机制,彻底搞懂这两个方法。


一、五分钟快速掌握核心区别

先上结论,后面再展开细节。

对比维度 sleep() wait()
所属类 Thread 类的静态方法 Object 类的实例方法
调用前提 任意地方均可调用 必须在同步块/同步方法中
是否释放锁 ❌ 不会释放任何锁 ✅ 释放当前对象的锁
唤醒方式 时间到自动唤醒 需要 notify / notifyAll 唤醒(也可超时唤醒)
典型场景 暂停执行、模拟耗时 线程间通信、等待条件满足

二、从代码看差异

2.1 sleep() 用法

java 复制代码
public class SleepDemo {
    public static void main(String[] args) {
        System.out.println("开始睡眠...");
        try {
            Thread.sleep(3000);  // 暂停 3 秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("醒了!");
    }
}

sleep() 可以在任何地方调用,它让当前线程 暂停指定时间,但不会释放它持有的任何锁。

2.2 wait() 用法

java 复制代码
public class WaitDemo {
    private static final Object lock = new Object();

    public static void main(String[] args) {
        synchronized (lock) {        // 必须要有 synchronized
            try {
                System.out.println("进入等待...");
                lock.wait();         // 释放 lock 锁,进入等待
                System.out.println("被唤醒,继续执行");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

如果去掉 synchronized 块,运行时会直接抛出:

复制代码
Exception in thread "main" java.lang.IllegalMonitorStateException

三、深入监视器:wait() 后的线程去哪儿了?

这是面试中经常追问的点。要回答清楚,需要理解 Java 对象监视器(Monitor)的内部结构。

3.1 每个对象都有两个"队列"

对于一个被 synchronized 修饰的对象,JVM 会为它维护两个队列:

队列名称 英文 存放的线程 线程状态
竞争队列 Entry Set / Contention List 想获取锁但还没抢到的线程 BLOCKED
等待队列 Wait Set 已经持有锁、但调用 wait() 主动放弃的线程 WAITING

3.2 队列流转图

复制代码
                    ┌─────────────────────────────────────┐
                    │           竞争队列 (Entry Set)       │
                    │    Thread-B (BLOCKED)                │
                    │    Thread-C (BLOCKED)                │
                    └─────────────────┬───────────────────┘
                                      │
                     锁释放后,JVM 从竞争队列选一个成为 Owner
                                      ▼
                    ┌─────────────────────────────────────┐
                    │         Monitor Owner               │
                    │      (当前持有锁的线程)               │
                    └─────────────────┬───────────────────┘
                                      │
                        owner 调用 wait() 后释放锁
                                      │
                                      ▼
                    ┌─────────────────────────────────────┐
                    │           等待队列 (Wait Set)        │
                    │    Thread-A (WAITING)                │
                    │    Thread-D (WAITING)                │
                    └─────────────────────────────────────┘

3.3 关键流转规则

  1. 竞争队列 → Owner:Owner 释放锁后,JVM 从竞争队列中选一个线程获得锁
  2. Owner → 等待队列 :Owner 调用 wait() → 释放锁 → 进入等待队列,状态变为 WAITING
  3. 等待队列 → 竞争队列 :被 notify / notifyAll 唤醒 → 移入竞争队列,状态变为 BLOCKED → 重新参与锁竞争

💡 注意:被 notify 唤醒后,线程并不会立即执行,它只是从"等待队列"进入了"竞争队列",需要重新竞争锁。竞争成功后才能从 wait() 方法返回。


四、wait/notify 实现线程间通信

这是生产者-消费者模式中最经典的协作方式。

4.1 完整示例

java 复制代码
public class WaitNotifyCommunication {

    private static final Object lock = new Object();
    private static boolean condition = false;   // 共享条件

    // 等待线程
    static class Waiter extends Thread {
        @Override
        public void run() {
            synchronized (lock) {
                while (!condition) {                    // ⚠️ 必须用 while,防止虚假唤醒
                    try {
                        System.out.println(Thread.currentThread().getName() + ":条件不满足,进入等待队列");
                        lock.wait();                    // 释放锁,进入 WAITING
                        System.out.println(Thread.currentThread().getName() + ":被唤醒,重新获得锁");
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
                System.out.println(Thread.currentThread().getName() + ":条件满足,继续执行任务");
            }
        }
    }

    // 通知线程
    static class Notifier extends Thread {
        @Override
        public void run() {
            synchronized (lock) {
                System.out.println(Thread.currentThread().getName() + ":修改条件");
                condition = true;
                System.out.println(Thread.currentThread().getName() + ":通知等待队列中的线程");
                lock.notify();      // 将等待队列中的一个线程移到竞争队列
                // 注意:此时被唤醒的线程还在竞争队列,并未立即执行
            }
            // 退出同步块后释放锁,被唤醒的线程才能竞争到锁并继续执行
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Waiter waiter = new Waiter();
        waiter.setName("Waiter");
        Notifier notifier = new Notifier();
        notifier.setName("Notifier");

        waiter.start();
        Thread.sleep(100);  // 确保 waiter 先拿到锁并进入等待
        notifier.start();
    }
}

执行结果:

复制代码
Waiter:条件不满足,进入等待队列
Notifier:修改条件
Notifier:通知等待队列中的线程
Waiter:被唤醒,重新获得锁
Waiter:条件满足,继续执行任务

4.2 为什么必须用 while 而不是 if?

java 复制代码
// ❌ 错误写法
if (!condition) {
    wait();
}

// ✅ 正确写法
while (!condition) {
    wait();
}

原因 :线程被唤醒后,条件可能再次不成立 (比如被另一个线程抢先修改了)。用 while 循环可以重新检查条件,这就是所谓的防范虚假唤醒。


五、一张表总结

对比项 sleep() wait()
定义位置 Thread 静态方法 Object 实例方法
所属 线程级别 对象级别
同步要求 无 必须在 synchronized 内
锁释放 不释放 释放当前对象锁
唤醒 自动唤醒 需要 notify / notifyAll
状态变化 RUNNABLE → TIMED_WAITING RUNNABLE → WAITING / TIMED_WAITING
典型用途 暂停线程、模拟耗时 等待条件、线程协作

六、思考题

如果线程 A 持有锁 lock1,然后调用 lock1.wait(),它会释放 lock1 的锁。但如果它同时还持有另一把锁 lock2,lock2 会被释放吗?

答案 :不会。wait() 只释放当前对象(lock1)的锁,其他锁不受影响。这也是为什么 wait/notify 必须成对使用同一个对象的原因。


写在最后

sleep() 和 wait() 看似都是让线程暂停,但本质上一个作用于线程本身,一个作用于对象监视器。理解了这两个方法的区别以及监视器的队列机制,Java 并发编程的很多问题都会变得清晰起来。

希望这篇文章对你有帮助。如果觉得不错,欢迎点赞、收藏、转发


相关推荐
小小数媒成员4 分钟前
UGUI原理(一)
开发语言·游戏·unity·游戏引擎
速易达网络24 分钟前
Three.js 动画入门教学:从场景搭建到流畅动效
开发语言·javascript·ecmascript
福大大架构师每日一题28 分钟前
excelize v2.11.1发布:Go 1.26.0起步,16项安全修复与嵌入对象能力全面升级
开发语言·安全·golang
Sarvartha30 分钟前
集合概述 & Collection 接口
开发语言
雪芽蓝域zzs33 分钟前
第 1 章:IDEA 创建 SpringBoot3.5.16 Maven 项目
java·maven·intellij-idea
golang学习记41 分钟前
IDEA 开始换芯,发布全新AI版终端
java·人工智能·intellij-idea
vipxieliang1 小时前
ValidX 正则表达式预编译优化:Pattern 复用策略
java·spring boot·后端
lhbweilai1 小时前
QT项目(串口助手)
开发语言·qt
eggwyw1 小时前
C++相交链表和反转链表详解
开发语言·c++
Wang's Blog1 小时前
Java 项目实战:交友项目 - FastDFS 工作原理
java·大数据·交友