Java Object notifyAll() 方法详解:多线程协作的“广播器”
在多线程编程的世界里,线程之间的协作就像一支乐队演奏交响乐。每个线程都是一个乐手,而协调演奏节奏的关键,就是“通知”机制。Java 提供了 notifyAll() 方法,正是这个“指挥棒”的核心工具之一。它让等待中的线程在条件满足时被唤醒,是实现线程同步的重要一环。
如果你正在学习多线程编程,那么 Java Object notifyAll() 方法 一定绕不开。它与 wait() 配合使用,构成了经典的等待-通知模式。今天我们就来深入剖析这个方法,从原理到实战,带你彻底掌握它的使用方式。
为什么需要 notifyAll() 方法?
在并发编程中,多个线程可能共享同一个资源。比如一个线程负责生产数据,另一个负责消费数据。如果数据池为空,消费线程就该等待;等生产线程生产了新数据后,就应该通知等待的消费线程。
但问题来了:如果只唤醒一个线程,可能会导致资源浪费或死锁。这时,notifyAll() 就派上用场了——它会唤醒所有因 wait() 而处于等待状态的线程。
想象一下:一个会议室里有 10 个人在等领导开会通知。如果领导只叫一个人,其他人还在等,会议室就空着,效率低下。而如果领导说“所有人注意,会议开始”,所有人就都醒了,可以开始讨论。notifyAll() 正是这个“广播”动作。
wait() 与 notifyAll() 的协同机制
wait() 和 notifyAll() 都是 Object 类的方法,必须在同步代码块(synchronized)中调用,否则会抛出 IllegalMonitorStateException。
核心规则:
- 调用
wait()前,线程必须持有该对象的锁(即进入 synchronized 块)。 wait()会释放锁,并让线程进入等待状态,直到被唤醒。notifyAll()必须在持有锁的情况下调用,它会唤醒所有正在等待该对象的线程。- 唤醒的线程不会立即执行,必须重新竞争锁。
代码示例:生产者-消费者模型
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不会重新检查条件。
最佳实践总结:
- 始终使用
while循环包装wait()。 notifyAll()优先于notify()。- 确保
wait()和notifyAll()在同一个锁对象上操作。 - 避免长时间持有锁,及时释放,提高并发性能。
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() 当作你线程协作的“广播器”,让程序运行得更流畅、更稳定。
无论你是初学者,还是正在优化现有系统,深入理解这个方法,都将为你的编码能力带来质的飞跃。