深入理解 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
状态变化 RUNNABLETIMED_WAITING RUNNABLEWAITING / TIMED_WAITING
典型用途 暂停线程、模拟耗时 等待条件、线程协作

六、思考题

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

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


写在最后

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

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


相关推荐
watson_pillow2 小时前
c++ 协程的初步理解
开发语言·c++
故事和你912 小时前
洛谷-算法1-2-排序2
开发语言·数据结构·c++·算法·动态规划·图论
皮皮林5512 小时前
面试官:ZSet 的底层实现是什么?
java
码云数智-大飞3 小时前
C++ RAII机制:资源管理的“自动化”哲学
java·服务器·php
2601_949816583 小时前
Spring+Quartz实现定时任务的配置方法
java
白毛大侠3 小时前
理解 Go 接口:eface 与 iface 的区别及动态性解析
开发语言·网络·golang
李昊哲小课3 小时前
Python办公自动化教程 - 第7章 综合实战案例 - 企业销售管理系统
开发语言·python·数据分析·excel·数据可视化·openpyxl
Hou'3 小时前
从0到1的C语言传奇之路
c语言·开发语言
不知名的老吴4 小时前
返回None还是空集合?防御式编程的关键细节
开发语言·python