wait 与 notify 详解
场景:小崔正在写飞翔科技的消息推送系统——后台线程生成推送消息,前台线程取消息发给用户。他自信满满地用了一个
while(true)循环不断检查队列:"有消息就发,没消息就睡一会儿……"白歌(看了一眼代码,眉头紧皱):"这叫什么设计?空转轮询 CPU 浪费不说,sleep 时间设短了空转,设长了延迟大——典型的'忙等'反模式。"
大翔:"那正确的做法是什么?"
白歌:"wait/notify。Java 线程间通信的标准方式。生产者没东西时 wait 释放锁睡觉,消费者消费完 notify 唤醒。零 CPU 消耗,即时响应。"
朱璐(认真补充):"但 wait/notify 用不好比轮询更危险——必须在 synchronized 里调用,wait 会释放锁,sleep 不会,还有虚假唤醒。"
Frank:"And in JDK 8, you should actually consider BlockingQueue or CompletableFuture for producer-consumer. But understanding wait/notify is fundamental — it's the foundation of all Java inter-thread communication."
小崔:"好,那我从 wait/notify 的底层机制开始学。"
一、定义速查表
| 属性 | 说明 |
|---|---|
| wait() | Object 方法,让当前线程等待,直到被 notify()/notifyAll() 唤醒或中断。必须在 synchronized 中调用,调用后释放锁并进入 WAITING 状态 |
| wait(long timeout) | 带超时的 wait,超时后自动唤醒。进入 TIMED_WAITING 状态 |
| wait(long timeout, int nanos) | 纳秒级超时,JDK 中实际按毫秒取整处理 |
| notify() | Object 方法,随机唤醒一个在该对象监视器上等待的线程。被唤醒线程不会立即执行,需要重新获取锁 |
| notifyAll() | Object 方法,唤醒所有在该对象监视器上等待的线程。推荐使用,避免"信号丢失"问题 |
| 监视器(Monitor) | 每个 Java 对象关联的同步机制,包含 EntryList(竞争锁队列)和 WaitSet(等待队列) |
| EntryList | 等待获取对象锁的线程队列(BLOCKED 状态)。竞争 synchronized 失败的线程进入此处 |
| WaitSet | 调用 wait() 后释放锁并等待的线程集合(WAITING 状态)。notify() 将线程从 WaitSet 移到 EntryList |
| 虚假唤醒(Spurious Wakeup) | 线程在没有收到 notify 的情况下被唤醒(OS 层面的原因)。必须用 while 循环检查条件,不能用 if |
| wait vs sleep | wait 是 Object 方法,释放锁,必须在 synchronized 中;sleep 是 Thread 方法,不释放锁,可在任意位置调用 |
二、Mermaid 图解
2.1 wait/notify 线程交互时序图
2.2 线程状态转换图(含 wait/notify)
三、wait/notify 核心规则
| 规则 | 说明 |
|---|---|
| 必须在 synchronized 中 | 调用 wait(), notify(), notifyAll() 前必须持有对象锁,否则抛出 IllegalMonitorStateException |
| wait() 释放锁 | wait() 调用后当前线程释放锁并进入 WAITING,被唤醒后需重新获取锁才能继续 |
| sleep() 不释放锁 | Thread.sleep() 让出 CPU 但不释放锁,其他线程无法进入 synchronized 块 |
| notify() 不立即释放锁 | notify() 只是把线程从 WaitSet 移到 EntryList,当前线程退出 synchronized 后才释放锁 |
| while 循环检查 | 必须用 while (条件) 而非 if (条件),防止虚假唤醒导致条件不满足时继续执行 |
| notifyAll() 优先 | 多线程等待时使用 notifyAll() 避免信号丢失,除非确定只有一个等待线程 |
四、完整代码示例
示例 1:飞翔科技消息队列 —— 生产者-消费者 wait/notify 实现
场景:飞翔科技的内部消息系统,大翔(工号 10001)作为生产者发布系统通知,白歌(工号 10002)作为消费者处理通知。消息队列容量为 5。
import java.util.LinkedList;
import java.util.Queue;
/**
* 飞翔科技 - 消息队列生产者消费者 wait/notify 实现
*
* 场景:大翔(10001) 生产消息,白歌(10002) 消费消息
* 队列容量 5,使用 wait/notifyAll 实现线程通信
*/
public class MessageQueueDemo {
static class MessageQueue {
private final Queue<String> queue = new LinkedList<>();
private final int capacity;
public MessageQueue(int capacity) {
this.capacity = capacity;
}
/**
* 生产者:大翔发布消息
* @param message 消息内容
* @param employeeId 员工工号
*/
public synchronized void produce(String message, int employeeId)
throws InterruptedException {
// ★ while 循环防止虚假唤醒 — 不能用 if!
while (queue.size() == capacity) {
System.out.printf("[生产者-工号%d] 队列已满(%d/%d),等待消费...\n",
employeeId, queue.size(), capacity);
wait(); // 释放锁,等待消费者唤醒
}
queue.offer(message);
System.out.printf("[生产者-工号%d] 发布: \"%s\" | 队列: %d/%d\n",
employeeId, message, queue.size(), capacity);
// ★ 唤醒所有等待线程 — 包括消费者和可能等待的其他生产者
notifyAll();
}
/**
* 消费者:白歌消费消息
* @param employeeId 员工工号
* @return 消费的消息
*/
public synchronized String consume(int employeeId)
throws InterruptedException {
// ★ while 循环防止虚假唤醒
while (queue.isEmpty()) {
System.out.printf("[消费者-工号%d] 队列为空,等待生产...\n", employeeId);
wait(); // 释放锁,等待生产者唤醒
}
String message = queue.poll();
System.out.printf("[消费者-工号%d] 消费: \"%s\" | 队列: %d/%d\n",
employeeId, message, queue.size(), capacity);
// ★ 唤醒所有等待线程 — 包括生产者和可能等待的其他消费者
notifyAll();
return message;
}
}
public static void main(String[] args) throws InterruptedException {
MessageQueue mq = new MessageQueue(5);
System.out.println("========== 飞翔科技内部消息系统 ==========\n");
// 生产者:大翔(10001)
Thread producer = new Thread(() -> {
try {
String[] messages = {
"系统维护通知:今晚 22:00 停机",
"薪资调整:本月起基本工资 8888.88 元",
"团建通知:周末南山烧烤",
"安全提醒:请更新登录密码",
"新项目立项:智能监控平台 v2.0",
"报销通知:本月报销截止 25 日",
"培训通知:周五 Java 并发编程培训",
"福利通知:下午茶已到茶水间",
"会议通知:周一 9:00 全体会议",
"年终奖通知:本月发放年终奖"
};
for (int i = 0; i < messages.length; i++) {
mq.produce(messages[i], 10001);
Thread.sleep(200); // 模拟生产间隔
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Producer-大翔");
// 消费者:白歌(10002)
Thread consumer = new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
String msg = mq.consume(10002);
Thread.sleep(400); // 模拟消费处理耗时
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Consumer-白歌");
producer.start();
consumer.start();
producer.join();
consumer.join();
System.out.println("\n========== 所有消息处理完毕 ==========");
}
}
控制台输出:
========== 飞翔科技内部消息系统 ==========
[生产者-工号10001] 发布: "系统维护通知:今晚 22:00 停机" | 队列: 1/5
[生产者-工号10001] 发布: "薪资调整:本月起基本工资 8888.88 元" | 队列: 2/5
[消费者-工号10002] 消费: "系统维护通知:今晚 22:00 停机" | 队列: 1/5
[生产者-工号10001] 发布: "团建通知:周末南山烧烤" | 队列: 2/5
[消费者-工号10002] 消费: "薪资调整:本月起基本工资 8888.88 元" | 队列: 1/5
[生产者-工号10001] 发布: "安全提醒:请更新登录密码" | 队列: 2/5
[生产者-工号10001] 发布: "新项目立项:智能监控平台 v2.0" | 队列: 3/5
[消费者-工号10002] 消费: "团建通知:周末南山烧烤" | 队列: 2/5
...(生产消费交替进行)
[生产者-工号10001] 发布: "年终奖通知:本月发放年终奖" | 队列: 1/5
[消费者-工号10002] 消费: "年终奖通知:本月发放年终奖" | 队列: 0/5
========== 所有消息处理完毕 ==========
示例 2:飞翔科技奇偶数交替打印 —— wait/notify 经典面试题
场景:两个线程交替打印 1-20 的奇偶数。大翔(10001)打印奇数,白歌(10002)打印偶数。使用 wait/notify 精确控制交替。
/**
* 飞翔科技 - 奇偶数交替打印 wait/notify 实现
*
* 场景:大翔(10001) 打印奇数,白歌(10002) 打印偶数
* 使用 wait/notify 实现交替输出 1-20
*/
public class OddEvenPrinterDemo {
// 共享计数器
private int number = 1;
private final int max;
private final Object lock = new Object();
public OddEvenPrinterDemo(int max) {
this.max = max;
}
/**
* 大翔打印奇数
*/
public void printOdd(int employeeId) {
synchronized (lock) {
while (number <= max) {
// ★ while 循环检查条件
while (number % 2 == 0) { // 当前是偶数,等待
try {
lock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
if (number > max) break;
System.out.printf("[工号%d-大翔] 奇数: %d\n", employeeId, number);
number++;
lock.notifyAll(); // ★ 唤醒偶数线程
}
}
}
/**
* 白歌打印偶数
*/
public void printEven(int employeeId) {
synchronized (lock) {
while (number <= max) {
// ★ while 循环检查条件
while (number % 2 == 1) { // 当前是奇数,等待
try {
lock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
if (number > max) break;
System.out.printf("[工号%d-白歌] 偶数: %d\n", employeeId, number);
number++;
lock.notifyAll(); // ★ 唤醒奇数线程
}
}
}
public static void main(String[] args) throws InterruptedException {
OddEvenPrinterDemo printer = new OddEvenPrinterDemo(20);
System.out.println("========== 飞翔科技奇偶数交替打印 (1-20) ==========\n");
// 大翔(10001) — 打印奇数
Thread oddThread = new Thread(
() -> printer.printOdd(10001), "Odd-大翔");
// 白歌(10002) — 打印偶数
Thread evenThread = new Thread(
() -> printer.printEven(10002), "Even-白歌");
oddThread.start();
evenThread.start();
oddThread.join();
evenThread.join();
System.out.println("\n========== 打印完成,工号10001月薪: 8888.88 ==========");
}
}
控制台输出:
========== 飞翔科技奇偶数交替打印 (1-20) ==========
[工号10001-大翔] 奇数: 1
[工号10002-白歌] 偶数: 2
[工号10001-大翔] 奇数: 3
[工号10002-白歌] 偶数: 4
[工号10001-大翔] 奇数: 5
[工号10002-白歌] 偶数: 6
[工号10001-大翔] 奇数: 7
[工号10002-白歌] 偶数: 8
[工号10001-大翔] 奇数: 9
[工号10002-白歌] 偶数: 10
[工号10001-大翔] 奇数: 11
[工号10002-白歌] 偶数: 12
[工号10001-大翔] 奇数: 13
[工号10002-白歌] 偶数: 14
[工号10001-大翔] 奇数: 15
[工号10002-白歌] 偶数: 16
[工号10001-大翔] 奇数: 17
[工号10002-白歌] 偶数: 18
[工号10001-大翔] 奇数: 19
[工号10002-白歌] 偶数: 20
========== 打印完成,工号10001月薪: 8888.88 ==========
五、wait vs sleep 深度对比
/**
* 飞翔科技 - wait vs sleep 锁释放行为对比
*/
public class WaitVsSleepDemo {
private static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
System.out.println("========== wait vs sleep 锁行为对比 ==========\n");
// === 测试1:sleep 不释放锁 ===
System.out.println("--- 测试1: sleep 不释放锁 ---");
Thread sleepThread = new Thread(() -> {
synchronized (lock) {
System.out.println("[sleep线程] 获取锁,开始 sleep 2 秒...");
try {
Thread.sleep(2000); // sleep 不释放锁!
} catch (InterruptedException e) { }
System.out.println("[sleep线程] sleep 结束,释放锁");
}
}, "SleepThread");
Thread tryLockThread = new Thread(() -> {
try { Thread.sleep(200); } catch (InterruptedException e) { }
System.out.println("[尝试线程] 尝试获取锁...");
long start = System.currentTimeMillis();
synchronized (lock) {
long waited = System.currentTimeMillis() - start;
System.out.printf("[尝试线程] 获取锁成功!等待了 %d ms\n", waited);
}
}, "TryLockThread");
sleepThread.start();
tryLockThread.start();
sleepThread.join();
tryLockThread.join();
// === 测试2:wait 释放锁 ===
System.out.println("\n--- 测试2: wait 释放锁 ---");
Thread waitThread = new Thread(() -> {
synchronized (lock) {
System.out.println("[wait线程] 获取锁,调用 wait()...");
try {
lock.wait(2000); // wait 释放锁!其他线程可以获取
} catch (InterruptedException e) { }
System.out.println("[wait线程] wait 返回,释放锁");
}
}, "WaitThread");
Thread tryLockThread2 = new Thread(() -> {
try { Thread.sleep(200); } catch (InterruptedException e) { }
System.out.println("[尝试线程2] 尝试获取锁...");
long start = System.currentTimeMillis();
synchronized (lock) {
long waited = System.currentTimeMillis() - start;
System.out.printf("[尝试线程2] 获取锁成功!等待了 %d ms\n", waited);
}
}, "TryLockThread2");
waitThread.start();
tryLockThread2.start();
waitThread.join();
tryLockThread2.join();
System.out.println("\n========== 结论 ==========");
System.out.println("sleep 不释放锁 → 其他线程被阻塞 2 秒");
System.out.println("wait 释放锁 → 其他线程可以立即获取");
}
}
控制台输出:
========== wait vs sleep 锁行为对比 ==========
--- 测试1: sleep 不释放锁 ---
[sleep线程] 获取锁,开始 sleep 2 秒...
[尝试线程] 尝试获取锁...
[sleep线程] sleep 结束,释放锁
[尝试线程] 获取锁成功!等待了 ~1800 ms ← sleep 不释放锁,等了近 2 秒
--- 测试2: wait 释放锁 ---
[wait线程] 获取锁,调用 wait()...
[尝试线程2] 尝试获取锁...
[尝试线程2] 获取锁成功!等待了 ~200 ms ← wait 释放锁,几乎立即获取
========== 结论 ==========
sleep 不释放锁 → 其他线程被阻塞 2 秒
wait 释放锁 → 其他线程可以立即获取
六、易错场景
❌ 错误 1:不在 synchronized 中调用 wait/notify
// ✗ 错误:不在 synchronized 中调用 wait() → IllegalMonitorStateException
Object lock = new Object();
lock.wait(); // 运行时异常!没有持有锁
// ✓ 正确:必须在 synchronized 块中
synchronized (lock) {
lock.wait(); // 合法
}
❌ 错误 2:用 if 而非 while 判断条件
// ✗ 错误:if 只检查一次,虚假唤醒后不重新检查
synchronized (lock) {
if (queue.isEmpty()) { // ★ 用 if!
lock.wait(); // 虚假唤醒后直接往下执行
}
queue.poll(); // 可能队列仍为空!
}
// ✓ 正确:用 while 循环,每次醒来都重新检查
synchronized (lock) {
while (queue.isEmpty()) { // ★ 用 while!
lock.wait();
}
queue.poll();
}
❌ 错误 3:notify 后不释放锁
// ✗ 错误:notify 后还持有锁,被唤醒线程无法立即执行
synchronized (lock) {
queue.offer(data);
lock.notify();
// 还在做一些耗时操作...被唤醒的线程必须等待!
Thread.sleep(5000); // 糟糕!
}
// ✓ 正确:notify 后尽快释放锁(最小化临界区)
synchronized (lock) {
queue.offer(data);
lock.notify();
} // 立即释放锁
❌ 错误 4:使用 notify() 而非 notifyAll() 导致信号丢失
// ✗ 错误:多生产者-多消费者用 notify() 可能唤醒同类型线程
// 场景:2 个生产者都在等"队列未满",2 个消费者都在等"队列非空"
// notify() 可能唤醒另一个生产者 → 检查队列仍满 → 继续 wait → 信号丢失!
// ✓ 正确:不确定时使用 notifyAll()
synchronized (lock) {
queue.offer(data);
lock.notifyAll(); // 唤醒所有等待线程,避免信号丢失
}
✅ 正确示范:wait/notify 模板代码
// ✓ 标准 wait/notify 模板
synchronized (lock) {
// 1. while 循环检查条件(防止虚假唤醒)
while (条件不满足) {
lock.wait(); // 2. 释放锁,进入等待
}
// 3. 条件满足,执行操作
doSomething();
// 4. 唤醒其他线程
lock.notifyAll(); // 5. 优先使用 notifyAll
}
七、面试考点
Q1:wait() 和 sleep() 的区别?
A:
| 对比维度 | wait() | sleep() |
|---|---|---|
| 所属类 | Object 实例方法 | Thread 静态方法 |
| 锁释放 | ✅ 释放锁 | ❌ 不释放锁 |
| 调用位置 | 必须在 synchronized 中 | 任意位置 |
| 唤醒方式 | notify()/notifyAll() 或超时 | 超时自动唤醒或 interrupt() |
| 线程状态 | WAITING / TIMED_WAITING | TIMED_WAITING |
| 使用场景 | 线程间通信(等待条件) | 暂停执行(延时) |
Q2:为什么 wait/notify 必须在 synchronized 中调用?
A:从两个层面理解:
语义层面:wait/notify 是线程间通信机制,需要基于共享状态。如果不在 synchronized 中,线程 A 调用 wait 时可能线程 B 刚刚修改了条件并调用了 notify(但 A 还没开始 wait),导致信号丢失(Lost Wakeup)。synchronized 保护了"检查条件 → wait"的原子性。
实现层面:JVM 的 Monitor 机制要求 wait/notify 操作 WaitSet,而 WaitSet 是 Monitor 的一部分。只有持有 Monitor 所有权的线程才能操作 WaitSet。否则抛出
IllegalMonitorStateException。
Q3:什么是虚假唤醒(Spurious Wakeup)?如何处理?
A:虚假唤醒指线程在没有被 notify/notifyAll 调用、没有被中断、也没有超时的情况下,从 wait() 返回。这是操作系统层面的现象(POSIX 条件变量规范允许),Java 为了兼容性也保留了这种行为。
处理方式:必须用 while 循环重新检查条件,而不是 if:
synchronized (lock) {
while (条件不满足) { // ★ 每次醒来都重新检查
lock.wait();
}
// 此时条件一定满足
}
Q4:notify() 和 notifyAll() 的区别?什么时候用哪个?
A:
- notify():随机唤醒一个在 WaitSet 中等待的线程,具体唤醒哪个由 JVM 决定
- notifyAll():唤醒 WaitSet 中所有等待线程,它们全部进入 EntryList 竞争锁
使用建议:
- 当所有等待线程等待的是同一条件时,可用 notify()(只有一个能被满足)
- 当等待线程等待的是不同条件或不确定时,必须用 notifyAll()
- 保守策略:始终使用 notifyAll(),避免信号丢失。只有在对线程模型完全可控时才用 notify()
大翔(靠在椅背上):"wait/notify 是 Java 线程通信的基石。虽然现在有 Lock、Condition、BlockingQueue,但面试必问,原理必须懂。"
白歌:"记住三个关键点:必须在 synchronized 中、wait 释放锁 sleep 不释放、while 循环防虚假唤醒。"
小崔:"那以后写生产者-消费者,我还用 wait/notify 吗?"
Frank:"In production code, use BlockingQueue or CompletableFuture. But for interviews and understanding the fundamentals, wait/notify is essential knowledge."
朱璐:"而且理解了 wait/notify,再去学 Condition、CountDownLatch、CyclicBarrier 这些高级并发工具,就事半功倍了。"