Java Object notifyAll() 方法(长文解析)

Java Object notifyAll() 方法详解:多线程协作的“广播器”

在多线程编程的世界里,线程之间的协作就像一支乐队演奏交响乐。每个线程都是一个乐手,而协调演奏节奏的关键,就是“通知”机制。Java 提供了 notifyAll() 方法,正是这个“指挥棒”的核心工具之一。它让等待中的线程在条件满足时被唤醒,是实现线程同步的重要一环。

如果你正在学习多线程编程,那么 Java Object notifyAll() 方法 一定绕不开。它与 wait() 配合使用,构成了经典的等待-通知模式。今天我们就来深入剖析这个方法,从原理到实战,带你彻底掌握它的使用方式。


为什么需要 notifyAll() 方法?

在并发编程中,多个线程可能共享同一个资源。比如一个线程负责生产数据,另一个负责消费数据。如果数据池为空,消费线程就该等待;等生产线程生产了新数据后,就应该通知等待的消费线程。

但问题来了:如果只唤醒一个线程,可能会导致资源浪费或死锁。这时,notifyAll() 就派上用场了——它会唤醒所有因 wait() 而处于等待状态的线程。

想象一下:一个会议室里有 10 个人在等领导开会通知。如果领导只叫一个人,其他人还在等,会议室就空着,效率低下。而如果领导说“所有人注意,会议开始”,所有人就都醒了,可以开始讨论。notifyAll() 正是这个“广播”动作。


wait() 与 notifyAll() 的协同机制

wait()notifyAll() 都是 Object 类的方法,必须在同步代码块(synchronized)中调用,否则会抛出 IllegalMonitorStateException

核心规则:

  1. 调用 wait() 前,线程必须持有该对象的锁(即进入 synchronized 块)。
  2. wait() 会释放锁,并让线程进入等待状态,直到被唤醒。
  3. notifyAll() 必须在持有锁的情况下调用,它会唤醒所有正在等待该对象的线程。
  4. 唤醒的线程不会立即执行,必须重新竞争锁。

代码示例:生产者-消费者模型

public class ProducerConsumer {
    private final Object lock = new Object();
    private boolean hasData = false;

    // 生产者线程:生产数据并通知等待的消费者
    public void produce() {
        synchronized (lock) {
            // 如果已有数据,等待
            while (hasData) {
                try {
                    System.out.println("生产者:数据已存在,等待消费...");
                    lock.wait(); // 释放锁,进入等待
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    System.out.println("生产者被中断");
                    return;
                }
            }

            // 模拟生产过程
            System.out.println("生产者:正在生产数据...");
            hasData = true;
            System.out.println("生产者:数据生产完成,通知所有等待者");

            // 通知所有等待的线程(消费者)
            lock.notifyAll();
        }
    }

    // 消费者线程:消费数据并通知等待的生产者
    public void consume() {
        synchronized (lock) {
            // 如果没有数据,等待
            while (!hasData) {
                try {
                    System.out.println("消费者:没有数据,等待生产...");
                    lock.wait(); // 释放锁,进入等待
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    System.out.println("消费者被中断");
                    return;
                }
            }

            // 模拟消费过程
            System.out.println("消费者:正在消费数据...");
            hasData = false;
            System.out.println("消费者:数据已消费,通知所有等待者");

            // 通知所有等待的线程(生产者)
            lock.notifyAll();
        }
    }

    public static void main(String[] args) {
        ProducerConsumer pc = new ProducerConsumer();

        // 启动多个生产者和消费者线程
        Thread producer = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                pc.produce();
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        });

        Thread consumer1 = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                pc.consume();
                try {
                    Thread.sleep(600);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        });

        Thread consumer2 = new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                pc.consume();
                try {
                    Thread.sleep(700);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        });

        // 启动所有线程
        producer.start();
        consumer1.start();
        consumer2.start();

        try {
            producer.join();
            consumer1.join();
            consumer2.join();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

代码说明

  • 使用 synchronized (lock) 保证线程安全。
  • while (hasData) 而不是 if:防止虚假唤醒(spurious wakeup)。
  • notifyAll() 唤醒所有等待线程,确保所有消费者都能有机会消费。
  • 线程被唤醒后,必须重新获取锁,才能继续执行。

notifyAll() 与 notify() 的区别

特性 notifyAll() notify()
唤醒数量 唤醒所有等待的线程 只唤醒一个线程
适用场景 多个线程可能都需要处理资源 只需一个线程处理即可
死锁风险 较低,所有线程都有机会竞争 较高,可能造成某些线程永远等待
性能开销 略高(唤醒多个线程) 较低

举个例子:

假设你有一个订单队列,多个订单处理线程在等待处理。当新订单到达时,你应该用 notifyAll(),因为多个处理线程都可以处理。如果只用 notify(),可能只有其中一个线程被唤醒,其他线程继续等待,造成资源浪费。

✅ 推荐:在大多数情况下,优先使用 notifyAll(),除非你明确知道只需要唤醒一个线程。


常见错误与最佳实践

错误 1:在非 synchronized 块中调用 wait() 或 notifyAll()

// ❌ 错误示例
public void badMethod() {
    Object obj = new Object();
    obj.wait(); // 抛出 IllegalMonitorStateException
}

✅ 正确做法:始终在 synchronized 块中调用。


错误 2:使用 if 判断条件,而非 while

// ❌ 错误示例
if (!hasData) {
    lock.wait(); // 可能虚假唤醒,导致错误执行
}

// ✅ 正确做法
while (!hasData) {
    lock.wait();
}

为什么?线程被唤醒后,可能因其他原因(如中断、虚假唤醒)继续执行,而 if 不会重新检查条件。


最佳实践总结:

  1. 始终使用 while 循环包装 wait()
  2. notifyAll() 优先于 notify()
  3. 确保 wait()notifyAll() 在同一个锁对象上操作
  4. 避免长时间持有锁,及时释放,提高并发性能。

notifyAll() 在真实场景中的应用

场景 1:缓存更新通知

当缓存数据发生变化时,需要通知所有监听该缓存的线程更新本地副本。使用 notifyAll() 可确保所有线程都能收到更新。

场景 2:任务调度器

任务调度器中,多个工作线程在等待新任务。当新任务到达时,调用 notifyAll() 让所有工作线程竞争任务。

场景 3:资源池管理

数据库连接池中,线程获取连接失败时等待。当连接被释放,调用 notifyAll() 通知所有等待线程尝试获取。

这些场景中,notifyAll() 都扮演着“全局广播”的角色,确保系统响应及时、资源利用高效。


性能考虑与替代方案

虽然 notifyAll() 功能强大,但也存在性能开销。如果有很多线程在等待,唤醒所有线程会导致“惊群效应”(thundering herd),即大量线程被唤醒但只有少数能获取锁。

替代方案建议:

  • 使用 ReentrantLock + Condition:更灵活,可精确控制唤醒线程。
  • 仅在必要时使用 notifyAll(),避免在高并发场景中频繁调用。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionExample {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    private boolean flag = false;

    public void signal() {
        lock.lock();
        try {
            flag = true;
            condition.signal(); // 只唤醒一个线程
        } finally {
            lock.unlock();
        }
    }

    public void await() {
        lock.lock();
        try {
            while (!flag) {
                condition.await(); // 等待
            }
            System.out.println("线程被唤醒");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            lock.unlock();
        }
    }
}

Condition 提供了比 notifyAll() 更精细的控制能力,适合复杂同步场景。


结语

Java Object notifyAll() 方法 是多线程编程中不可或缺的工具。它通过唤醒所有等待线程,确保资源的高效利用和系统响应的及时性。尽管它有性能开销,但在大多数场景下,其带来的可靠性远超代价。

作为开发者,掌握它的使用方式、理解其背后的原理,并避免常见陷阱,是迈向高级并发编程的重要一步。从今天起,把 notifyAll() 当作你线程协作的“广播器”,让程序运行得更流畅、更稳定。

无论你是初学者,还是正在优化现有系统,深入理解这个方法,都将为你的编码能力带来质的飞跃。