wait/notify vs Condition:从监视器到管程的进化
问题
Object.wait()/notify() 和 Condition.await()/signal() 有什么区别?为什么说 Condition 更灵活?
一个经典问题,两种写法
先看一个生产者-消费者队列的两种实现,这是最直观的对比入口。
用 synchronized + wait/notify
public class BoundedBufferV1 {
private final Object[] items = new Object[10];
private int putIndex, takeIndex, count;
private final Object monitor = new Object();
public void put(Object item) throws InterruptedException {
synchronized (monitor) {
while (count == items.length) {
monitor.wait(); // 队列满,等待
}
items[putIndex] = item;
if (++putIndex == items.length) putIndex = 0;
count++;
monitor.notifyAll(); // 通知消费者
}
}
public Object take() throws InterruptedException {
synchronized (monitor) {
while (count == 0) {
monitor.wait(); // 队列空,等待
}
Object item = items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length) takeIndex = 0;
count--;
monitor.notifyAll(); // 通知生产者
}
return item;
}
}这段代码能工作,但有问题:每次 notifyAll() 都会唤醒所有等待线程,包括生产者和消费者。生产者被唤醒后发现队列还是满的,又回去 wait;消费者被唤醒后发现队列还是空的,也回去 wait。多余的唤醒 → 多余的上下文切换 → 性能浪费。
用 ReentrantLock + Condition
public class BoundedBufferV2 {
private final Object[] items = new Object[10];
private int putIndex, takeIndex, count;
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public void put(Object item) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await(); // 只等"不满"信号
}
items[putIndex] = item;
if (++putIndex == items.length) putIndex = 0;
count++;
notEmpty.signal(); // 只通知消费者
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await(); // 只等"不空"信号
}
Object item = items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length) takeIndex = 0;
count--;
notFull.signal(); // 只通知生产者
} finally {
lock.unlock();
}
return item;
}
}多个 Condition 把"队列满"和"队列空"两个等待条件分开,signal() 只唤醒对应等待队列的线程。不需要唤醒所有线程再让大部分人回去继续等。
核心差异
| 维度 | wait/notify | Condition |
|---|---|---|
| 绑定锁 | 必须配合 synchronized | 配合 Lock 接口 |
| 条件队列数量 | 每个对象只有 1 个隐式队列 | 一个 Lock 可创建多个显式队列 |
| 唤醒方式 | notify() 随机选一个,notifyAll() 唤醒全部 | signal() 精确唤醒目标队列 |
| 中断响应 | wait() 抛出 InterruptedException | awaitInterruptibly() 支持,awaitUninterruptibly() 不响应 |
| 超时等待 | wait(long timeout) | awaitNanos(long)、awaitUntil(Date) 更精确 |
| 是否支持公平等待 | 否 | 取决于 Lock 的公平性设置 |
深层原理:一个条件队列 vs 多个条件队列
wait/notify 的底层机制
每个 Java 对象都关联一个 ObjectMonitor,其内部有三个关键队列:
_WaitSet:条件等待队列(调用wait()的线程进入)_EntryList:同步阻塞队列(竞争锁失败的线程进入)_owner:当前持有锁的线程
wait() 将当前线程封装成 ObjectWaiter 节点放入 _WaitSet,释放锁,然后挂起。notify() 从 _WaitSet 随机选一个节点转移到 _EntryList,等待它重新竞争锁。
问题在于:只有一个 _WaitSet。生产者和消费者混在同一个等待队列里,notifyAll() 不得不把所有人全拉出来,即使绝大部分人都不该被唤醒。
Condition 的底层机制
每个 Condition 绑定一个 AQS 的条件队列(单向链表,节点复用 AQS 的 Node):
// AbstractQueuedSynchronizer.ConditionObject 内部
public class ConditionObject implements Condition {
private transient Node firstWaiter; // 条件队列头
private transient Node lastWaiter; // 条件队列尾
// ...
}await() 的流程:
- 当前线程包装成 Node,加入条件队列尾部
- 释放锁(AQS 的 state 归零)
- 调用
LockSupport.park(this)挂起 - 被唤醒后检查是否在同步队列中,不在则继续 park
signal() 的流程:
- 检查当前线程是否持有锁(否则抛
IllegalMonitorStateException) - 将条件队列头节点转移到同步队列(
transferForSignal) - 被转移的节点在同步队列中等待锁,获取到锁后从
await()返回
关键设计:多个 Condition 有多个条件队列,notFull.signal() 只影响 notFull 的条件队列,notEmpty 队列里的消费者线程完全不受影响。
虚假唤醒:为什么 while 不能写成 if
// 错误写法
if (count == 0) {
notEmpty.await(); // 醒来后可能 count 还是 0!
}
// 正确写法
while (count == 0) {
notEmpty.await();
}wait/await 返回时,条件不一定成立。原因有两个:
- 虚假唤醒(Spurious Wakeup):操作系统在某些情况下会无故唤醒等待线程,即使没有收到 signal/notify。POSIX 标准允许这种行为,Java 继承了这一特性。
- 多线程竞争:两个消费者同时被唤醒,其中一个抢到锁消费了最后一个元素,另一个拿到锁时队列已经空了。
所以 wait/await 必须放在 while 循环中,醒来后重新检查条件。这是并发编程的铁律,不是可选项。
超时控制的精度差异
// wait 的超时精度受限于 JVM 实现
synchronized (lock) {
lock.wait(1000); // 最多等 1 秒,但可能提前返回
}
// Condition 提供更细粒度的超时
lock.lock();
try {
// 纳秒级超时
boolean timedOut = notFull.awaitNanos(TimeUnit.SECONDS.toNanos(1)) <= 0;
// 指定绝对时间
boolean timedOut = !notFull.awaitUntil(new Date(System.currentTimeMillis() + 1000));
} finally {
lock.unlock();
}Condition 的 awaitNanos 返回剩余纳秒数,可以精确判断是被 signal 唤醒还是超时唤醒,这在实现带超时的分布式锁、连接池等场景中非常有用。
什么时候用 wait/notify,什么时候用 Condition
适合 wait/notify 的场景:
- 简单的同步场景,只有一个等待条件
- 代码量小,不想引入 Lock 接口
- 性能要求不高,可读性优先
适合 Condition 的场景:
- 多个等待条件(BoundedQueue、读写锁)
- 需要精确的超时控制
- 需要中断响应或不可中断等待
- 配合公平锁使用
总结
Object.wait/notify基于 JVM 的ObjectMonitor,每个对象只有一个隐式条件队列Condition基于 AQS,一个 Lock 可创建多个显式条件队列,实现精确唤醒- 虚假唤醒对两者都存在,
wait/await必须在 while 循环中检查条件 Condition提供更丰富的 API:不可中断等待、纳秒级超时、绝对时间超时- 选择哪个取决于场景:简单同步用 wait/notify,多条件/高并发场景用 Condition