在 Java 多线程开发中,wait()、notify() 和 notifyAll() 是最经典但也最容易写错的一套线程通信机制。
它们用于解决:当线程因为"条件不满足"无法继续执行时,如何安全地等待并在条件改变后继续执行。
这篇文章将从原理、流程、示例到最佳实践,把这三兄弟一次讲透。
一、为什么需要 wait / notify?(最根本的问题)
在多线程场景中,经常会出现:
- 消费者想消费,但仓库空了
- 生产者想生产,但仓库满了
- 主线程需要等待子线程准备好某个结果
- 条件未满足前,线程无法继续往下执行
如果用 while(true) 死循环检查:
while (item == 0) {
// 忙等(Busy waiting),疯狂消耗 CPU
}
这会导致:
- CPU 空转
- 性能极差
- 线程争抢激烈
于是,Java 提供了 基于锁对象的条件等待机制:
当条件不满足时,线程主动挂起自己,并释放锁;当条件满足时,被其他线程唤醒继续执行。
这就是 wait / notify。
二、wait / notify 的正确理解(核心概念)
1. wait() 的本质动作
当线程执行:
lock.wait();
它会做 三件事:
- 当前线程暂停执行(挂起)
- 释放 lock 的那把锁(让其他线程有机会修改状态)
- 把自己加入 lock 对象的"等待队列(Wait Set)"
线程会一直睡在那里,直到某个线程执行了:
lock.notify()或lock.notifyAll()
并且:
- 🔥 唤醒的线程必须重新抢 lock 的锁
- 抢到锁之后,才能继续执行 wait 后面的代码
2. notify() / notifyAll() 做了什么?
两个方法都必须在同步代码块中调用:
synchronized(lock) {
lock.notify();
}
否则会抛:
IllegalMonitorStateException
notify() 作用:
-
从 lock 的等待队列中 随机唤醒 1 个线程
-
但唤醒 ≠ 立即执行
-
被唤醒线程要在当前线程释放锁后才能抢锁执行
notifyAll() 作用:
-
唤醒等待队列中的所有线程
-
所有线程一起去抢锁,成功者继续执行
实际开发中一般推荐 notifyAll():更安全,避免唤醒错误线程导致死锁。
三、完整流程图解(非常关键)
假设线程 A 想消费商品,但仓库空了:
线程 A 做的事:
-
synchronized(lock)→ 拿到锁 -
判断仓库是否为空 → 是空的
-
调用
wait():-
A 挂起
-
A 释放锁
-
A 进入等待队列
-
线程 B(生产者)做的事:
-
synchronized(lock)→ 拿到锁 -
生产商品
-
调用
notify()或notifyAll():唤醒 A -
B 退出 synchronized → 释放锁
最后:
-
A 被唤醒
-
抢到锁后,继续执行 wait() 下一行的代码
四、生产者消费者完整 Demo(最经典示例)
java
class Depot {
private int item = 0;
private final Object lock = new Object();
public void produce() throws InterruptedException {
synchronized (lock) {
while (item == 1) { // 仓库满 → 等待
lock.wait();
}
item = 1;
System.out.println("生产了一个商品");
lock.notifyAll(); // 通知消费者
}
}
public void consume() throws InterruptedException {
synchronized (lock) {
while (item == 0) { // 仓库空 → 等待
lock.wait();
}
item = 0;
System.out.println("消费了一个商品");
lock.notifyAll(); // 通知生产者
}
}
}
为什么要用 while 而不是 if?
因为:
-
Java 的 wait 存在 虚假唤醒(Spurious Wakeups)
-
被唤醒后必须重新判断条件
这是 JDK 官方文档明确要求的。
五、wait 和 sleep 有什么区别?(面试必考)
| 特性 | wait | sleep |
|---|---|---|
| 属于谁? | Object | Thread |
| 是否释放锁? | 释放 | 不释放 |
| 是否必须在 synchronized 中? | 必须 | 不需要 |
| 唤醒方式 | notify/notifyAll | 到点自动醒 |
一句话总结:
sleep 是让线程睡觉
wait 是让线程到等待队列里等别人叫醒
六、常见错误理解(90% 的人会踩坑)
❌ 1. 认为 notify 会立即让对方执行
错误!
notify 只是"叫醒",对方必须等待 当前线程释放锁 后才能执行。
❌ 2. 不用 while,用 if 判断条件
会导致唤醒后条件不满足却继续执行 → 出 Bug
❌ 3. 以为 wait 是"做完事情后休息"
完全相反!
wait 是因为做不下去,不是因为做完了。
❌ 4. wait 和 notify 不在同一个锁对象上
比如:
java
a.wait();
b.notify();
永远唤不醒。
七、总结(建议背下来)
wait 的本质:
线程因为条件不满足 → 挂起自己 → 释放锁 → 进入等待队列 → 等别人唤醒。
notify / notifyAll 的本质:
条件改变 → 唤醒等待队列里的线程,让它们重新抢锁继续执行。
wait / notify 是同步代码里的"条件等待机制",不是休眠机制。
八、这套机制与 RxJava 背压有什么关系?
其实本质一样:
-
RxJava 背压:下游忙 → 上游暂停、等待或限速
-
wait/notify:条件不满足 → 当前线程暂停等待其他线程改变条件
都是:
生产速度与消费速度不一致时的流量控制(Flow Control)思想。