生产者-消费者模型
理解 notify() 的最佳方式就是通过 生产者-消费者 模型。

想象一个场景:
- 生产者:不断生产产品,并把产品放入一个有限的仓库。
- 消费者:不断从仓库中取出产品进行消费。
这个“有限的仓库”就是线程间协作的关键,如果仓库满了,生产者就必须等待,直到消费者取走产品,仓库有空间了才能继续生产,如果仓库空了,消费者就必须等待,直到生产者放入产品,仓库有货了才能继续消费。
wait() 和 notify() 就是用来实现这种等待和通知机制的。
wait() 和 notify() 的基本用法
wait() 和 notify() 都不是定义在 Thread 类中,而是定义在 Object 类里,这是因为任何对象都可以作为锁(监视器),而等待和通知操作必须与锁关联。

wait() 方法
当一个线程调用某个对象的 wait() 方法时,它会做以下几件事:
- 释放当前持有的该对象的锁。
- 进入该对象的等待队列,并进入阻塞状态。
- 等待其他线程来唤醒它。
注意:wait() 必须在同步代码块或同步方法中调用,否则会抛出 IllegalMonitorStateException。
notify() 方法
当一个线程调用某个对象的 notify() 方法时,它会做以下几件事:
- 随机唤醒在该对象等待队列中的一个线程。
- 被唤醒的线程会尝试重新获取该对象的锁。
- 一旦锁被成功获取,被唤醒的线程就会从
wait()调用的地方继续执行。
注意:
notify()不会释放锁,它只是发出一个“唤醒”信号,锁是在同步代码块/方法执行完毕后,或者被持有锁的线程主动wait()或release时才释放。- 被唤醒的线程不会立即获得锁,它需要和所有其他正在竞争该锁的线程(包括新来的线程)一起去竞争。
代码示例:一个简单的生产者-消费者
下面是一个使用 wait() 和 notify() 实现的生产者-消费者模型,仓库最多只能存储 1 个产品。
// 仓库类,作为共享资源
class Warehouse {
private int product = 0; // 0表示仓库为空,1表示仓库有货
// 消费者调用
public synchronized void consume() {
// 如果仓库为空,消费者等待
if (product == 0) {
try {
System.out.println("消费者:仓库为空,我等待...");
wait(); // 释放锁,并进入等待队列
System.out.println("消费者:被唤醒了,开始消费!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 消费产品
System.out.println("消费者:消费产品,仓库清空。");
product = 0;
// 唤醒可能在等待的生产者
System.out.println("消费者:通知生产者可以生产了。");
notify(); // 唤醒一个等待的线程
}
// 生产者调用
public synchronized void produce() {
// 如果仓库有货,生产者等待
if (product == 1) {
try {
System.out.println("生产者:仓库已满,我等待...");
wait(); // 释放锁,并进入等待队列
System.out.println("生产者:被唤醒了,开始生产!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 生产产品
System.out.println("生产者:生产产品,仓库已满。");
product = 1;
// 唤醒可能在等待的消费者
System.out.println("生产者:通知消费者可以消费了。");
notify(); // 唤醒一个等待的线程
}
}
public class ProducerConsumerDemo {
public static void main(String[] args) {
Warehouse warehouse = new Warehouse();
// 创建消费者线程
new Thread(() -> {
for (int i = 0; i < 5; i++) {
warehouse.consume();
try { Thread.sleep(500); } catch (InterruptedException e) {}
}
}, "消费者").start();
// 创建生产者线程
new Thread(() -> {
for (int i = 0; i < 5; i++) {
warehouse.produce();
try { Thread.sleep(500); } catch (InterruptedException e) {}
}
}, "生产者").start();
}
}
可能的执行流程分析:
- 初始状态:
product = 0,仓库为空。 - 消费者先运行:进入
consume()方法,发现product == 0,调用wait()。消费者线程释放锁并进入等待状态。 - 生产者运行:获取
Warehouse对象的锁,进入produce()方法,发现product == 0,开始生产,生产后product = 1,然后调用notify()。 notify()的作用:notify()会唤醒消费者线程(因为它是唯一在等待的线程)。生产者线程此时仍然持有锁,所以消费者线程虽然被唤醒,但无法立即执行,必须等到produce()方法执行完毕,生产者线程释放锁。- 消费者获得锁并执行:生产者线程执行完毕,释放锁,被唤醒的消费者线程重新获得锁,从
wait()的下一句代码开始执行,打印“被唤醒了...”,消费产品,product变为 0,然后调用notify()(虽然此时没有线程在等待,但调用它无害)。 - 循环往复:这个过程不断重复,实现了生产者和消费者的交替工作。
notify() 的重要特性与风险
1 唤醒的随机性
notify() 会随机唤醒一个等待的线程,如果你有多个消费者线程在等待,notify() 可能会唤醒任何一个,这可能导致“虚假唤醒”或唤醒了错误的线程。
例子:假设有 2 个消费者线程 C1 和 C2 在等待,1 个生产者线程 P。
- P 生产了一个产品,调用
notify()。 - 假设它随机唤醒了 C1,C1 获得锁,消费产品,然后释放锁。
- C2 仍然在等待,但仓库已经是空的,C2 不再检查条件,它可能会被其他
notify()唤醒,然后直接尝试消费一个不存在的产品,导致错误。
2 虚假唤醒
即使在没有调用 notify() 的情况下,等待的线程也有极小概率会被意外唤醒,这是 JVM 的一个特性,虽然不常见,但我们必须考虑。
解决方案:永远使用 while 循环检查条件
为了解决上述两个问题,wait() 调用必须放在一个 while 循环中,而不是 if 语句中,每次被唤醒时,都必须重新检查等待条件是否满足。
修正后的 consume 方法:
public synchronized void consume() {
// 使用 while 循环,而不是 if
while (product == 0) { // 被唤醒后,再次检查条件
try {
System.out.println("消费者:仓库为空,我等待...");
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 只有当条件满足时,才会执行下面的代码
System.out.println("消费者:消费产品,仓库清空。");
product = 0;
notify();
}
这样,即使线程被 notify() 唤醒,或者发生了虚假唤醒,它也会再次进入 while 循环,发现仓库还是空的,于是会再次调用 wait(),继续等待,只有当生产者真正放入了产品并调用了 notify(),并且该线程重新获得锁后,while 条件才会不成立,线程才会继续执行消费逻辑。
notify() vs notifyAll()
| 特性 | notify() |
notifyAll() |
|---|---|---|
| 唤醒线程数 | 随机唤醒一个等待线程 | 唤醒所有在该对象上等待的线程 |
| 效率 | 效率更高,因为只唤醒一个线程 | 效率较低,需要唤醒所有线程,它们会竞争锁 |
| 安全性 | 有风险,可能唤醒错误的线程,导致逻辑复杂 | 更安全,确保所有相关的线程都被检查条件,不会错过通知 |
| 使用场景 | 当你确切知道只有一个线程在等待,或者你不关心唤醒哪个线程,且唤醒的线程一定能处理当前情况时。 | 当你不确定有多少线程在等待,或者你需要唤醒所有相关线程来重新评估它们的状态时。在大多数情况下,notifyAll() 是更安全的选择。 |
何时使用 notifyAll()?
一个典型的场景是:当某个共享资源的状态发生改变时,所有等待这个资源的线程(无论是生产者还是消费者)都应该被唤醒,去重新检查这个状态。
在一个线程池中,当一个任务完成时,可能需要唤醒所有等待任务的线程,让它们去重新检查是否有新任务。
wait() 的三个版本
Object 类还提供了另外两个 wait() 方法:
wait(long timeout):等待指定的时间(毫秒),如果在超时时间内没有被其他线程唤醒,它会自动苏醒。wait(long timeout, int nanos):等待指定的时间(毫秒 + 纳秒),精度更高。
这两个方法可以用来避免线程永久等待(即“死等”),如果一个生产者线程因为某种原因崩溃了,那么所有等待它的消费者线程将永远等待下去,使用带超时的 wait() 可以确保线程最终会苏醒,从而可以处理一些异常情况或重新尝试。
总结与最佳实践
- 基本规则:
wait()和notify()必须在同步代码块或同步方法中调用,操作的是同一个对象锁。 wait()的作用:释放锁,让当前线程进入等待状态。notify()的作用:随机唤醒一个等待线程,但不立即释放锁。- 防止死锁和错误:
wait()调用必须包裹在while循环中,用于在被唤醒后重新检查等待条件,这是防止“虚假唤醒”和“唤醒错误线程”导致问题的关键。 - 选择
notify()还是notifyAll():- 优先考虑
notifyAll(),因为它更安全,能避免复杂的逻辑错误。 - 只有在经过仔细分析,确定
notify()不会带来问题(只有一个消费者线程)时,才使用notify()以提高效率。
- 优先考虑
- 现代替代方案:虽然
wait/notify是 Java 的基础,但在现代 Java 开发中,更推荐使用高级并发工具,如java.util.concurrent包中的BlockingQueue、CountDownLatch、CyclicBarrier、Semaphore等,它们已经封装好了复杂的线程同步逻辑,使用起来更简单、更安全、不易出错。
使用 BlockingQueue 实现生产者-消费者会非常简洁:
// 生产者
new Thread(() -> {
try {
queue.put("产品"); // 如果队列满,put() 方法会自动阻塞
} catch (InterruptedException e) { /* ... */ }
}).start();
// 消费者
new Thread(() -> {
try {
String product = queue.take(); // 如果队列空,take() 方法会自动阻塞
// 消费产品...
} catch (InterruptedException e) { /* ... */ }
}).start(); 