notify 和 notifyAll
notify()会随机唤醒一个等待在当前对象 “等待池(wait set)” 中的线程,其他等待线程依然留在等待池中,直到被再次唤醒(notify/notifyAll)或中断。会唤醒所有等待在当前对象 “等待池(wait set)” 中的线程,所有等待线程都会从WAITING转为BLOCKED状态,参与锁的竞争。notify()随机唤醒一个等待线程,性能略优但易导致 “丢失唤醒”,仅适用于单线程等
·
前置核心前提
在讲区别前,必须先明确这两个方法的共同调用规则(也是新手最容易踩坑的点):
- 都是
Object类的实例方法(所有对象都有),必须通过 “等待线程绑定的那个对象” 调用(比如线程调用lock.wait(),就必须用lock.notify()/lock.notifyAll()唤醒); - 调用时必须持有该对象的 synchronized 锁(在 synchronized 块 / 方法中调用),否则抛出
IllegalMonitorStateException; - 调用后不会立即释放锁:只是 “发送唤醒通知”,锁要等当前线程退出同步块 / 方法后才释放;
- 被唤醒的线程不会立即执行:会从
WAITING/TIMED_WAITING状态转为BLOCKED状态,参与锁的竞争,只有抢到锁才能继续执行。
一、notify () 详解
1. 核心定义
notify() 会随机唤醒一个等待在当前对象 “等待池(wait set)” 中的线程,其他等待线程依然留在等待池中,直到被再次唤醒(notify/notifyAll)或中断。
2. 关键特性
- 唤醒数量:仅唤醒一个线程,JVM 随机选择(无优先级、无顺序,完全随机);
- 未唤醒线程:剩余等待线程不受影响,继续处于
WAITING状态,不会被唤醒; - 风险点:若唤醒的线程检查条件后发现仍不满足(比如生产者没生产出数据),再次调用
wait(),而其他等待线程永远没被唤醒,会导致线程永久等待(丢失唤醒)。
3. 代码示例:notify () 仅唤醒一个线程
public class NotifyDemo {
private static final Object lock = new Object();
// 模拟共享资源:0表示无数据,1表示有数据
private static int resource = 0;
public static void main(String[] args) {
// 创建3个消费者线程(都等待resource=1)
for (int i = 1; i <= 3; i++) {
new Thread(() -> {
synchronized (lock) {
// 用while循环检查条件(避免虚假唤醒)
while (resource == 0) {
try {
System.out.println(Thread.currentThread().getName() + ":无数据,进入等待");
lock.wait(); // 进入wait set
System.out.println(Thread.currentThread().getName() + ":被唤醒,重新检查数据");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 消费数据
resource = 0;
System.out.println(Thread.currentThread().getName() + ":消费数据,resource重置为0");
}
}, "消费者" + i).start();
}
// 生产者线程:生产数据后调用notify()
new Thread(() -> {
try {
Thread.sleep(1000); // 确保消费者都进入等待
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock) {
resource = 1; // 生产数据
System.out.println("\n生产者:生产数据,resource=1,调用notify()");
lock.notify(); // 仅唤醒一个消费者
System.out.println("生产者:notify()调用完成,释放锁\n");
}
}, "生产者").start();
}
}
运行结果(核心特征)
只有一个消费者被唤醒并消费数据,另外两个消费者永久等待(不会输出):
消费者1:无数据,进入等待
消费者2:无数据,进入等待
消费者3:无数据,进入等待
生产者:生产数据,resource=1,调用notify()
生产者:notify()调用完成,释放锁
消费者2:被唤醒,重新检查数据
消费者2:消费数据,resource重置为0
// 消费者1、3永远停留在等待状态,无输出
二、notifyAll () 详解
1. 核心定义
notifyAll() 会唤醒所有等待在当前对象 “等待池(wait set)” 中的线程,所有等待线程都会从 WAITING 转为 BLOCKED 状态,参与锁的竞争。
2. 关键特性
- 唤醒数量:唤醒全部等待线程,无遗漏;
- 执行顺序:被唤醒的线程会竞争同一把锁,同一时刻只有一个线程能抢到锁执行,其他线程仍处于
BLOCKED状态,直到抢到锁; - 安全性:即使部分线程检查条件不满足再次
wait(),其他线程仍有机会抢到锁并检查条件,不会出现 “永久等待”; - 性能:唤醒所有线程会增加锁竞争,但安全性远高于
notify(),是实际开发中的首选。
3. 代码示例:notifyAll () 唤醒所有线程
仅修改生产者线程的 notify() 为 notifyAll(),其余代码和上面一致:
// 生产者线程中修改这一行
lock.notifyAll(); // 唤醒所有消费者
运行结果(核心特征)
所有消费者都被唤醒,依次竞争锁并检查条件:
消费者1:无数据,进入等待
消费者2:无数据,进入等待
消费者3:无数据,进入等待
生产者:生产数据,resource=1,调用notifyAll()
生产者:notifyAll()调用完成,释放锁
消费者1:被唤醒,重新检查数据
消费者1:消费数据,resource重置为0
消费者2:被唤醒,重新检查数据
消费者2:无数据,进入等待
消费者3:被唤醒,重新检查数据
消费者3:无数据,进入等待
核心解读:
- 消费者 1 抢到锁,消费数据后
resource=0; - 消费者 2、3 随后抢到锁,检查到
resource=0,再次进入等待(无永久等待); - 若后续生产者再次生产数据并调用
notifyAll(),消费者 2、3 会再次被唤醒。
三、notify () vs notifyAll () 核心对比表
| 维度 | notify() | notifyAll() |
|---|---|---|
| 唤醒数量 | 随机唤醒一个等待线程 | 唤醒所有等待线程 |
| 等待线程处理 | 未被唤醒的线程永久留在 wait set | 所有线程被唤醒,转为 BLOCKED 抢锁 |
| 风险点 | 易导致 “丢失唤醒”(线程永久等待) | 无丢失唤醒风险,安全性高 |
| 锁竞争 | 竞争少,性能略优(但风险大) | 竞争多,性能略低(但安全) |
| 适用场景 | 明确只有一个线程在等待(如单生产者单消费者) | 多个线程等待同一条件(如多生产者多消费者) |
| 虚假唤醒处理 | 依赖 while 检查条件,但仍可能丢失唤醒 | 依赖 while 检查条件,无丢失唤醒风险 |
四、高频易错点与最佳实践
1. 必须用 while 循环检查条件(无论用哪个方法)
即使调用 notifyAll(),也必须用 while (条件不满足) 而非 if 检查条件,因为:
- 线程可能被 “虚假唤醒”(JVM 无原因唤醒线程);
- 被唤醒的线程抢到锁时,条件可能已被其他线程修改(比如示例中消费者 2、3 被唤醒时,数据已被消费者 1 消费)。
// 错误写法(if)
if (resource == 0) { lock.wait(); }
// 正确写法(while)
while (resource == 0) { lock.wait(); }
2. notify () 的 “丢失唤醒” 风险示例
假设两个消费者等待同一数据,生产者调用 notify() 随机唤醒消费者 A:
- 消费者 A 检查到数据存在,消费后数据为空;
- 消费者 B 永远没被唤醒,永久等待(丢失唤醒);
- 而
notifyAll()会唤醒 A 和 B,B 检查到数据为空后再次等待,无永久等待风险。
3. 选择原则:优先用 notifyAll ()
- 除非你能100% 确定只有一个线程在等待该对象(比如单生产者单消费者模型),否则一律用
notifyAll(); - 不要为了 “性能” 选择
notify():锁竞争的性能损耗远小于 “线程永久等待” 导致的程序异常。
4. 唤醒后锁的释放时机
调用 notify()/notifyAll() 后,当前线程不会立即释放锁,必须等退出 synchronized 块 / 方法后,锁才会释放,被唤醒的线程才能抢锁。
synchronized (lock) {
lock.notifyAll(); // 仅发送通知,锁未释放
Thread.sleep(2000); // 锁仍被持有,被唤醒的线程无法抢锁
} // 退出同步块,锁才释放,被唤醒的线程开始抢锁
总结
notify()随机唤醒一个等待线程,性能略优但易导致 “丢失唤醒”,仅适用于单线程等待场景;notifyAll()唤醒所有等待线程,无丢失唤醒风险,是多线程等待场景的首选;- 无论用哪种方法,都必须用
while循环检查条件,避免虚假唤醒,且调用时必须持有对象锁。
更多推荐

所有评论(0)