前置核心前提

在讲区别前,必须先明确这两个方法的共同调用规则(也是新手最容易踩坑的点):

  1. 都是 Object 类的实例方法(所有对象都有),必须通过 “等待线程绑定的那个对象” 调用(比如线程调用 lock.wait(),就必须用 lock.notify()/lock.notifyAll() 唤醒);
  2. 调用时必须持有该对象的 synchronized 锁(在 synchronized 块 / 方法中调用),否则抛出 IllegalMonitorStateException
  3. 调用后不会立即释放锁:只是 “发送唤醒通知”,锁要等当前线程退出同步块 / 方法后才释放;
  4. 被唤醒的线程不会立即执行:会从 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); // 锁仍被持有,被唤醒的线程无法抢锁
} // 退出同步块,锁才释放,被唤醒的线程开始抢锁

总结

  1. notify() 随机唤醒一个等待线程,性能略优但易导致 “丢失唤醒”,仅适用于单线程等待场景;
  2. notifyAll() 唤醒所有等待线程,无丢失唤醒风险,是多线程等待场景的首选;
  3. 无论用哪种方法,都必须用 while 循环检查条件,避免虚假唤醒,且调用时必须持有对象锁。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐