Skip to content

wait/notify vs Condition:从监视器到管程的进化

问题

Object.wait()/notify()Condition.await()/signal() 有什么区别?为什么说 Condition 更灵活?

一个经典问题,两种写法

先看一个生产者-消费者队列的两种实现,这是最直观的对比入口。

用 synchronized + wait/notify

java
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

java
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/notifyCondition
绑定锁必须配合 synchronized配合 Lock 接口
条件队列数量每个对象只有 1 个隐式队列一个 Lock 可创建多个显式队列
唤醒方式notify() 随机选一个,notifyAll() 唤醒全部signal() 精确唤醒目标队列
中断响应wait() 抛出 InterruptedExceptionawaitInterruptibly() 支持,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):

java
// AbstractQueuedSynchronizer.ConditionObject 内部
public class ConditionObject implements Condition {
    private transient Node firstWaiter;  // 条件队列头
    private transient Node lastWaiter;   // 条件队列尾
    // ...
}

await() 的流程:

  1. 当前线程包装成 Node,加入条件队列尾部
  2. 释放锁(AQS 的 state 归零)
  3. 调用 LockSupport.park(this) 挂起
  4. 被唤醒后检查是否在同步队列中,不在则继续 park

signal() 的流程:

  1. 检查当前线程是否持有锁(否则抛 IllegalMonitorStateException
  2. 将条件队列头节点转移到同步队列(transferForSignal
  3. 被转移的节点在同步队列中等待锁,获取到锁后从 await() 返回

关键设计:多个 Condition 有多个条件队列,notFull.signal() 只影响 notFull 的条件队列,notEmpty 队列里的消费者线程完全不受影响。

虚假唤醒:为什么 while 不能写成 if

java
// 错误写法
if (count == 0) {
    notEmpty.await();  // 醒来后可能 count 还是 0!
}
// 正确写法
while (count == 0) {
    notEmpty.await();
}

wait/await 返回时,条件不一定成立。原因有两个:

  1. 虚假唤醒(Spurious Wakeup):操作系统在某些情况下会无故唤醒等待线程,即使没有收到 signal/notify。POSIX 标准允许这种行为,Java 继承了这一特性。
  2. 多线程竞争:两个消费者同时被唤醒,其中一个抢到锁消费了最后一个元素,另一个拿到锁时队列已经空了。

所以 wait/await 必须放在 while 循环中,醒来后重新检查条件。这是并发编程的铁律,不是可选项。

超时控制的精度差异

java
// 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();
}

ConditionawaitNanos 返回剩余纳秒数,可以精确判断是被 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

手撕 → 框架 → 生产化,一步步把 AI Agent 工程化搞透。