wait和notify这个为什么要在synchronized代码块中?
Java中wait()和notify()必须在synchronized代码块内使用,主要出于三个原因:一是防止"丢失唤醒"问题,确保条件检查和等待操作的原子性;二是操作对象监视器的前提要求,只有持有锁的线程才能操作等待队列;三是避免虚假唤醒后的逻辑错误。wait()会释放当前锁,唤醒后需重新竞争锁。正确用法是在synchronized块内用while循环检查条件,保证线程安全。
一、核心结论先明确
wait()和notify()的本质是操作对象的监视器(Monitor),而只有当前线程获取到该对象的 Monitor 锁(也就是进入synchronized代码块 / 方法),才能合法操作这个监视器。如果不在synchronized中调用,会直接抛出IllegalMonitorStateException运行时异常。
二、深层原因:为什么要这样设计?
核心是为了避免线程安全问题(尤其是 “丢失唤醒”),保证 “条件判断 + 等待 / 唤醒” 的原子性。
原因 1:避免 “丢失唤醒”(Lost Wakeup)问题
这是最核心的原因。我们先看一个 “不加 synchronized 会出问题” 的场景:假设存在一个 “生产者 - 消费者” 模型,消费者线程要等生产者生产数据后才能执行:
错误示例(无 synchronized):
// 共享变量:是否有数据
private static boolean hasData = false;
private static final Object lock = new Object();
// 消费者线程
Thread consumer = new Thread(() -> {
// 步骤1:检查条件
if (!hasData) {
// 步骤2:准备等待(此时线程可能被CPU切换)
lock.wait(); // 直接抛IllegalMonitorStateException
}
System.out.println("消费数据");
});
// 生产者线程
Thread producer = new Thread(() -> {
hasData = true;
lock.notify(); // 直接抛IllegalMonitorStateException
System.out.println("生产数据");
});
即使不抛异常,也会出现致命问题:
- 消费者线程执行完
if (!hasData)(条件为 true),但还没执行wait()时,CPU 切换到生产者线程; - 生产者线程设置
hasData=true,并调用notify()(此时没有线程在等待,notify “白发” 了); - 消费者线程回到 CPU,执行
wait(),但此时 notify 已经发过了,消费者会永久等待(丢失了这次唤醒)。
加 synchronized 后的正确逻辑:
// 消费者线程(正确版)
Thread consumer = new Thread(() -> {
synchronized (lock) { // 获取锁,保证条件检查+wait原子性
while (!hasData) { // 用while而非if,防止虚假唤醒
lock.wait(); // 释放锁,进入等待
}
hasData = false;
System.out.println("消费数据");
}
});
// 生产者线程(正确版)
Thread producer = new Thread(() -> {
synchronized (lock) { // 获取锁,保证修改条件+notify原子性
hasData = true;
lock.notify(); // 唤醒等待线程
System.out.println("生产数据");
}
});
synchronized保证了:
- 消费者的 “条件检查(
!hasData) +wait()” 是原子操作,不会被生产者线程打断; - 生产者的 “修改条件(
hasData=true) +notify()” 也是原子操作,不会被消费者线程打断; - 彻底避免 “丢失唤醒” 问题。
原因 2:操作对象监视器(Monitor)的前提
Java 中每个对象都关联一个 “监视器(Monitor)”,这个监视器是实现同步和线程通信的核心:
- 调用
synchronized (obj)时,线程会尝试获取obj的 Monitor 锁; wait():让当前线程释放 Monitor 锁,并进入该对象的 “等待队列”;notify():从该对象的等待队列中唤醒一个线程,被唤醒的线程需要重新竞争 Monitor 锁。
如果线程没有获取到 Monitor 锁(即不在synchronized中),就没有权限操作这个 Monitor 的等待队列,JVM 会直接抛出IllegalMonitorStateException—— 这是 JVM 的强制规则,本质是保证只有 “持有锁的线程” 才能操作锁的等待队列。
原因 3:防止 “虚假唤醒” 后的逻辑错误
即使加了synchronized,我们也会用while循环检查条件(而非if),这是为了防止 “虚假唤醒”(线程被唤醒后,条件可能已经不满足)。而synchronized保证了循环检查条件时的线程安全:
// 错误:用if,虚假唤醒后直接执行后续逻辑
if (!hasData) { lock.wait(); }
// 正确:用while,唤醒后重新检查条件
while (!hasData) { lock.wait(); }
三、关键补充:wait () 的核心特性
很多人误以为wait()是 “暂停线程”,但其实:
wait()调用时,会立即释放当前持有的 Monitor 锁(这是和Thread.sleep()的核心区别 ——sleep 不释放锁);- 线程被 notify () 唤醒后,不会立即执行,而是需要重新竞争Monitor 锁,只有获取到锁后,才能从 wait () 处继续执行;
notifyAll()会唤醒所有等待该锁的线程,这些线程会竞争锁,只有一个能获取到。
总结
- 核心目的:避免 “丢失唤醒” 问题,保证 “条件判断 + 等待 / 唤醒” 的原子性,这是多线程通信的线程安全基础;
- 底层规则:wait/notify 操作的是对象的 Monitor(监视器),必须先通过 synchronized 获取该 Monitor 锁,否则抛 IllegalMonitorStateException;
- 最佳实践:wait () 必须放在 synchronized 代码块中,且用 while 循环检查条件(而非 if),防止虚假唤醒。
更多推荐



所有评论(0)