在 Java 多线程编程中,notify()
和 notifyAll()
是用于线程间通信的关键方法,它们与 wait()
方法配合使用,协调多个线程对共享资源的访问。以下是它们的区别、应用场景及底层机制的详细解析:
1. 核心区别
方法 | 行为 | 适用场景 |
---|---|---|
notify() | 随机唤醒一个正在该对象上等待的线程(由 JVM 决定唤醒哪一个)。 | 所有等待线程的唤醒条件相同,且只需一个线程处理任务(如单资源释放)。 |
notifyAll() | 唤醒所有正在该对象上等待的线程,线程需重新竞争锁。 | 等待线程的唤醒条件不同,或需要多个线程协作处理(如多条件判断、资源批量分配)。 |
2. 底层机制
- 锁竞争:
notify()
仅唤醒一个线程,减少了锁竞争和上下文切换开销。notifyAll()
唤醒所有线程,但最终只有一个线程能获得锁,其他线程会重新阻塞,可能导致性能损耗。
- 条件队列:
- 每个对象的监视器锁(Monitor)关联一个等待队列(存放调用
wait()
的线程)。 notify()
将队列中的一个线程移到锁的入口队列(Entry Set)。notifyAll()
将所有等待线程移到入口队列,重新竞争锁。
- 每个对象的监视器锁(Monitor)关联一个等待队列(存放调用
3. 经典应用场景
场景 1:单资源释放(notify()
更高效)
问题:多个线程等待使用唯一资源(如数据库连接池中的一个连接)。
解决:释放资源后只需唤醒一个等待线程。
class ConnectionPool {
private final Object lock = new Object();
private boolean isAvailable = true;
public void useConnection() {
synchronized (lock) {
while (!isAvailable) {
try {
lock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
isAvailable = false;
// 使用连接...
}
}
public void releaseConnection() {
synchronized (lock) {
isAvailable = true;
lock.notify(); // 唤醒一个等待线程
}
}
}
场景 2:多条件判断(必须用 notifyAll()
)
问题:线程等待不同条件(如生产者-消费者模型中,缓冲区满时唤醒消费者,空时唤醒生产者)。
解决:唤醒所有线程,确保正确类型的线程能响应条件变化。
class BoundedBuffer {
private final Object lock = new Object();
private final Queue<Integer> buffer = new LinkedList<>();
private final int CAPACITY = 10;
public void produce(int value) throws InterruptedException {
synchronized (lock) {
while (buffer.size() == CAPACITY) {
lock.wait(); // 缓冲区满,生产者等待
}
buffer.add(value);
lock.notifyAll(); // 唤醒所有(可能有消费者在等待)
}
}
public int consume() throws InterruptedException {
synchronized (lock) {
while (buffer.isEmpty()) {
lock.wait(); // 缓冲区空,消费者等待
}
int value = buffer.poll();
lock.notifyAll(); // 唤醒所有(可能有生产者在等待)
return value;
}
}
}
4. 常见陷阱与解决方案
陷阱 1:错误使用 notify()
导致线程饥饿
- 现象:某些线程因未被唤醒而永久等待。
- 解决:确保所有相关条件变化时使用
notifyAll()
,或明确知道仅需唤醒一个线程。
陷阱 2:虚假唤醒(Spurious Wakeup)
- 现象:线程未收到通知却从
wait()
返回(底层 OS 机制导致)。 - 解决:始终在循环中检查条件,而非
if
语句:synchronized (lock) { while (!condition) { // 必须用 while,不能用 if lock.wait(); } // 处理任务 }
5. 性能考量
场景 | notify() | notifyAll() |
---|---|---|
竞争程度 | 低(仅一个线程被唤醒) | 高(所有线程竞争锁) |
上下文切换 | 较少 | 较多(但最终只有一个线程运行) |
适用性 | 单条件、单一任务类型 | 多条件、复杂协作场景 |
6. 最佳实践
- 默认使用
notifyAll()
:
除非明确只需唤醒一个线程,否则优先使用notifyAll()
,避免因条件遗漏导致线程饥饿。 - 封装条件检查:
将条件判断和等待逻辑封装到方法中,确保代码可维护性。 - 结合
java.util.concurrent
工具:
使用Lock
、Condition
等高级 API(替代synchronized
+wait/notify
),提供更精细的控制:Lock lock = new ReentrantLock(); Condition notEmpty = lock.newCondition(); Condition notFull = lock.newCondition(); public void produce(int value) { lock.lock(); try { while (buffer.size() == CAPACITY) { notFull.await(); // 等待非满条件 } buffer.add(value); notEmpty.signalAll(); // 唤醒消费者 } finally { lock.unlock(); } }
总结
notify()
:轻量级唤醒,适合单一条件、单任务场景,需确保不会遗漏任何必要唤醒。notifyAll()
:安全唤醒所有线程,适合多条件协作或不确定唤醒哪个线程的场景,但可能增加竞争开销。- 核心原则:
- 使用
while
循环检查条件,避免虚假唤醒。 - 在复杂条件或多类型等待任务中,优先选择
notifyAll()
或Condition
对象。
- 使用