← 返回题目列表

wait/notify 的用法是什么?为什么必须在同步块里?和 Condition 有什么区别?

高频 中等 第 16 / 31 题 更新于 2026/07/26
waitnotify生产者消费者Condition

简化版

wait/notify/notifyAll 是 Object 的方法,用于线程间等待-通知协作:wait() 让线程释放锁并进入等待,notify()/notifyAll() 唤醒等待的线程。它们必须在 synchronized 块内调用——因为要操作对象的「监视器锁」,不持锁调用会抛 IllegalMonitorStateException。用它们能手写生产者消费者,但要注意:判断条件必须用 while 循环(防虚假唤醒)、优先用 notifyAllCondition(配合 ReentrantLock)是它们的升级版,能创建多个等待队列,精确唤醒某一类线程。

详细版

三个方法(都属于 Object,因为锁是绑在对象上的):

  • wait():当前线程释放锁、进入该对象的等待队列、阻塞,直到被 notify 或中断;
  • notify():随机唤醒该对象等待队列里的一个线程;
  • notifyAll():唤醒等待队列里的所有线程(更安全,推荐)。

为什么必须在 synchronized 里:wait/notify 操作的是对象的监视器(monitor)。调用前必须持有该对象的锁,否则 JVM 无法保证「检查条件 → 等待」这一过程的原子性,会抛 IllegalMonitorStateException

标准生产者消费者写法

class Buffer {
    private final Queue<Integer> queue = new LinkedList<>();
    private final int cap = 10;

    public synchronized void put(int x) throws InterruptedException {
        while (queue.size() == cap) {   // 必须用 while,不能用 if
            wait();                     // 满了:释放锁并等待
        }
        queue.add(x);
        notifyAll();                    // 唤醒消费者
    }

    public synchronized int take() throws InterruptedException {
        while (queue.isEmpty()) {       // 必须用 while
            wait();                     // 空了:释放锁并等待
        }
        int x = queue.poll();
        notifyAll();                    // 唤醒生产者
        return x;
    }
}

⚠️ 判断条件必须用 while 而不是 if。因为线程被唤醒后要重新竞争锁、且可能是「虚假唤醒」或条件又被别的线程改变,while 能保证醒来后重新检查条件,if 则会跳过检查直接执行、导致 bug。

完整版教学

一、为什么 wait/notify 是 Object 的方法而不是 Thread 的

这是高频追问。答案藏在「锁的本质」里:Java 里任何对象都可以作为锁(synchronized(obj)),每个对象都有一个与之关联的「监视器(monitor)」和「等待队列」。

wait/notify 操作的是「对象的等待队列」,不是「线程」:
  obj.wait()   → 把当前线程放进 obj 的等待队列
  obj.notify() → 从 obj 的等待队列里唤醒一个线程
既然等待队列是绑在对象上的,方法自然定义在 Object 上,任何对象都能调

如果 wait/notify 定义在 Thread 上,就没法表达「在某个具体对象的锁上等待」了。锁绑在对象上,等待/通知也就绑在对象上——所以它们是 Object 的方法。这也解释了下一个问题:为什么必须持有那个对象的锁才能调。

二、为什么必须在 synchronized 块内调用

wait/notify 必须在持有对象锁的前提下调用,否则抛 IllegalMonitorStateException。根本原因是避免「丢失唤醒(lost wakeup)」

假设 wait/notify 不需要锁,考虑这个时序:
线程A(消费者)              线程B(生产者)
if (queue.isEmpty())
                            queue.add(x)
                            notify()      ← 此时 A 还没 wait,通知丢了!
  wait()                    ← A 现在才等,但通知已经错过,永久阻塞

必须在同一把锁保护下,"检查条件"和"进入等待"才是原子的:
线程A: lock → 检查空 → wait(原子地释放锁+等待)
线程B: lock(要等A释放)→ add → notify → unlock

关键机制wait()原子地「释放锁 + 进入等待」,这样 notify 线程才能拿到锁去改条件、再通知。如果不强制持锁,「检查条件」和「wait」之间会有窗口,通知可能在这个窗口里发出而被错过——线程就永远醒不来了。synchronized 把这个窗口消除了。

三、while vs if:虚假唤醒与条件失效

判断条件必须用 while 包住 wait(),这是并发编程的铁律。两个原因:

原因1:虚假唤醒(spurious wakeup)
  操作系统层面,线程可能在没有 notify 的情况下被唤醒(底层实现允许)
  → 醒来后必须重新检查条件,用 while 就会再判一次

原因2:条件可能又变了
  notifyAll 唤醒多个消费者,它们排队抢锁;
  第一个消费者拿到锁取走了唯一的元素,第二个拿到锁时队列又空了
  → 若用 if,第二个不会重新检查,直接 poll 出 null / 越界
if (queue.isEmpty()) wait();   // ✗ 危险:醒来不重新检查
while (queue.isEmpty()) wait();// ✓ 正确:醒来重新检查条件

一句话:wait 醒来后一切都可能变了,必须重新验证条件,所以永远用 while。

四、notify vs notifyAll:为什么优先 notifyAll

notify() 只随机唤醒一个等待线程,notifyAll() 唤醒全部。看似 notify 更省,但它容易导致「信号丢失/死锁」:

场景:一个对象上同时有生产者和消费者在 wait
队列满,2 个生产者 P1、P2 在 wait,0 个消费者在 wait
消费者取走一个元素后调 notify():
  → 随机唤醒了 P1?好,P1 继续生产 —— 正常
  → 但如果队列里既有等待的生产者又有等待的消费者,
    notify 可能唤醒"错误类型"的线程(唤醒了另一个消费者,它发现还是空又继续 wait),
    真正该醒的生产者没被叫醒 → 可能全体沉睡(死锁)

notifyAll 把所有等待者都叫醒,它们重新检查各自条件(while),该干活的干活、不该动的继续 wait,不会漏掉。代价是有些线程被无谓唤醒又睡下(一点性能损耗)。除非你非常确定等待队列里都是同类线程,否则一律用 notifyAll。

五、Condition:wait/notify 的精确版

Condition(由 ReentrantLock.newCondition() 创建)是 wait/notify 的升级。核心优势:一把锁可以创建多个 Condition(多个等待队列),实现精确唤醒

class Buffer {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();   // 生产者等这个
    private final Condition notEmpty = lock.newCondition();  // 消费者等这个

    public void put(int x) throws InterruptedException {
        lock.lock();
        try {
            while (full()) notFull.await();    // 生产者只在 notFull 上等
            enqueue(x);
            notEmpty.signal();                 // 精确唤醒消费者(不吵醒其他生产者)
        } finally { lock.unlock(); }
    }
    // take 对称:在 notEmpty 上 await,signal notFull
}

对比 wait/notify 的关键差异:

维度wait/notifyCondition
依附synchronized 锁ReentrantLock
等待队列数一个对象一个一把锁可建多个
唤醒精度notifyAll 唤醒所有(含无关线程)signal 精确唤醒某一类
方法wait/notify/notifyAllawait/signal/signalAll

有了 notFull、notEmpty 两个队列,生产者和消费者分开等,signal 能精确叫醒对应一方,避免了 notifyAll 的「全部叫醒再重判」的浪费。JUC 的 ArrayBlockingQueue 就是这么实现的。

六、更上层的选择:别自己造轮子

wait/notify 和 Condition 是底层原语,实际业务里能用现成工具就别手写

需求                          首选方案
生产者-消费者缓冲             BlockingQueue(内部已用 Condition 封装好)
等一批任务完成               CountDownLatch
限制并发数                   Semaphore
异步结果编排                 CompletableFuture

手写 wait/notify 容易漏 while、错用 notify、锁范围不对。面试要能写、能讲原理,但工程上优先用 BlockingQueue 这类封装——它把「等待/唤醒/加锁」都藏好了,put/take 即可。理解 wait/notify 的价值在于看懂这些工具的底层

记忆钩子:「wait 释放锁进等待、notify 唤醒、必须在 synchronized 里(防丢唤醒)、条件用 while(防虚假唤醒)、优先 notifyAll;Condition 是多队列精确版」

七、常见误区与追问

  • 误区:wait/notify 可以在 synchronized 外调用。 不行,会抛 IllegalMonitorStateException;必须持有该对象的锁,以保证检查条件和等待的原子性。
  • 误区:用 if 判断条件就够了。 必须用 while——防虚假唤醒,以及 notifyAll 后条件可能又被别的线程改变,醒来要重新检查。
  • 误区:notify 比 notifyAll 好,因为省。 notify 随机唤醒一个,可能叫醒错误类型的线程导致信号丢失/死锁;不确定时一律用 notifyAll。
  • 误区:wait() 不释放锁。 wait 会原子地释放锁并进入等待,否则 notify 线程拿不到锁,无法通知;这正是它和 sleep 的关键区别(sleep 不释放锁)。
  • 追问:wait 和 sleep 有什么区别? wait 是 Object 方法、释放锁、需在同步块内、靠 notify 唤醒;sleep 是 Thread 静态方法、不释放锁、可在任意位置、到时自动醒。
  • 追问:Condition 相比 wait/notify 的最大优势? 一把锁可创建多个 Condition(多个等待队列),能用 signal 精确唤醒某一类线程(如只唤醒消费者),避免 notifyAll 无差别唤醒的浪费。
  • 追问:signal 和 signalAll 对应 notify 和 notifyAll 吗? 是的,语义一一对应;但因 Condition 可分队列,signal 唤醒的是「本条件队列」里的线程,比 notify 精确,误唤醒风险更低。

八、加强记忆

wait/notify 是线程「等待-通知」的底层原语,记牢五个要点:它们是 Object 的方法(因为锁和等待队列绑在对象上);必须在 synchronized 块内调用(否则抛 IllegalMonitorStateException,本质是让「检查条件+进入等待」原子化,防止 notify 在检查和 wait 之间发出而丢失唤醒);wait()原子地释放锁并等待(这是它区别于 sleep 的核心);判断条件必须用 while(防虚假唤醒 + 防条件被别的线程改回去);唤醒优先 notifyAll(notify 随机唤醒可能叫错线程导致死锁)。Condition(ReentrantLock.newCondition)是它的升级版——一把锁可建多个等待队列(如 notFull/notEmpty),用 signal 精确唤醒某一类线程,ArrayBlockingQueue 就靠它。工程上优先用 BlockingQueue 等封装好的工具,手写 wait/notify 只在面试和理解底层时用。一句话「Object 方法、锁内调用、wait 释放锁、while 判条件、notifyAll 优先,Condition 是多队列精确版」。