前言
看似都是"让线程停下来",背后的原理却完全不同
在 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 关键流转规则
- 竞争队列 → Owner:Owner 释放锁后,JVM 从竞争队列中选一个线程获得锁
- Owner → 等待队列 :Owner 调用
wait()→ 释放锁 → 进入等待队列,状态变为WAITING - 等待队列 → 竞争队列 :被
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 并发编程的很多问题都会变得清晰起来。
希望这篇文章对你有帮助。如果觉得不错,欢迎点赞、收藏、转发