核心概念:为什么需要 wait() 和 notify()?
想象一个生产者-消费者的场景:

- 生产者:负责生产数据,并放入一个缓冲区(比如一个队列)。
- 消费者:负责从缓冲区取出数据并进行处理。
这里存在一个竞态条件:
- 如果缓冲区满了,生产者必须等待,直到消费者取走数据,腾出空间。
- 如果缓冲区是空的,消费者必须等待,直到生产者放入新数据。
如果使用简单的 if 判断,会发生什么?
- 虚假唤醒:线程可能在没有任何通知的情况下,意外地从等待状态中唤醒,如果使用
if,它被唤醒后会直接执行后续代码,而不会再次检查条件,这可能导致数据不一致或错误。 - 竞态窗口:在
if条件判断为真后,到wait()调用前,另一个线程可能已经修改了条件,导致当前线程进入不必要的等待。
为了解决这些问题,Java 提供了基于监视器的等待/通知机制。
wait() 方法
当一个线程调用某个对象的 wait() 方法时,它会做以下几件事:

- 释放当前对象的锁:这是最关键的一点,调用
wait()的线程必须先获取到该对象的锁,否则会抛出IllegalMonitorStateException异常。 - 进入该对象的等待队列:线程会进入该对象的
wait set,并暂时停止执行。 - 等待被唤醒:它会一直阻塞,直到发生以下情况之一:
- 另一个线程调用了该对象的
notify()或notifyAll()方法。 - 另一个线程中断了该等待线程。
- 发生了虚假唤醒。
- 另一个线程调用了该对象的
wait() 的三个版本
void wait(): 无限期等待,直到被notify()或notifyAll()唤醒。void wait(long timeout): 等待指定毫秒数,如果超时,线程会自动被唤醒(就像被notify()了一样)。void wait(long timeout, int nanos): 等待指定毫秒和纳秒数,更精确的超时控制。
最佳实践:
wait()总是应该放在一个while循环中,而不是if语句中,这是为了防止虚假唤醒,确保线程在被唤醒后,再次检查等待条件是否仍然满足。
notify() 和 notifyAll() 方法
这两个方法用于唤醒在某个对象上等待的线程,调用它们的线程必须已经获取到该对象的锁,否则会抛出 IllegalMonitorStateException 异常。
notify()
- 作用:随机唤醒在该对象等待队列中的一个线程,具体唤醒哪个线程,是由 JVM 决定的,具有不确定性。
- 特点:它只唤醒一个线程,如果唤醒的线程发现条件仍然不满足(缓冲区仍然是空的),它会再次调用
wait()并继续等待,这可能会导致“活锁”(Livelock)的情况,即某些线程一直无法获得执行机会。
notifyAll()
- 作用:唤醒在该对象等待队列中的所有等待线程。
- 特点:所有被唤醒的线程都会去竞争该对象的锁,只有一个线程能竞争成功并继续执行,其他未成功的线程会再次进入阻塞状态,等待下一次锁的释放。
- 优点:
notifyAll()更安全,因为它能确保至少有一个线程(其等待条件被满足的线程)能够继续执行,避免了notify()可能导致的线程饥饿问题。
经典模式:while 循环检查
这是使用 wait/notify 的黄金法则,无论是生产者还是消费者,其等待逻辑都应该遵循这个模式。
// 获取锁
synchronized (lockObject) {
// 1. 检查等待条件,条件不满足则等待
while (!conditionIsMet) {
try {
// 2. 调用 wait() 释放锁并进入等待状态
lockObject.wait();
} catch (InterruptedException e) {
// 处理中断
Thread.currentThread().interrupt();
return;
}
}
// 3. 条件满足,执行核心业务逻辑
// ... do something ...
}
为什么必须是 while 循环?

- 防止虚假唤醒:JVM 规范允许在没有明确通知的情况下,线程被唤醒。
while循环可以确保即使被虚假唤醒,线程也会再次检查条件,如果条件不满足,就继续等待。 - 防止条件变化:在
if判断为真后,到wait()调用前,另一个线程可能已经修改了条件,导致当前线程进入不必要的等待。while循环可以捕获到这种变化。
代码示例:生产者-消费者模型
下面是一个使用 wait() 和 notifyAll() 实现的生产者-消费者模型。
import java.util.LinkedList;
import java.util.Queue;
import java.util.Random;
class ProducerConsumerExample {
// 共享资源,使用一个队列作为缓冲区
private final Queue<Integer> buffer = new LinkedList<>();
// 缓冲区最大容量
private final int CAPACITY = 5;
public static void main(String[] args) {
ProducerConsumerExample example = new ProducerConsumerExample();
// 启动生产者线程
new Thread(example.new Producer(), "Producer-Thread").start();
// 启动消费者线程
new Thread(example.new Consumer(), "Consumer-Thread").start();
}
// 生产者
class Producer implements Runnable {
private final Random random = new Random();
@Override
public void run() {
try {
while (true) {
produce(random.nextInt(100));
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void produce(int item) throws InterruptedException {
// 使用 buffer 对象作为锁
synchronized (buffer) {
// 检查缓冲区是否已满,如果满了就等待
while (buffer.size() == CAPACITY) {
System.out.println("Buffer is full, Producer is waiting...");
buffer.wait(); // 释放 buffer 的锁,并进入等待
}
System.out.println("Producer produced: " + item);
buffer.add(item);
// 通知可能在等待的消费者
buffer.notifyAll();
}
// 模拟生产耗时
Thread.sleep(1000);
}
}
// 消费者
class Consumer implements Runnable {
@Override
public void run() {
try {
while (true) {
consume();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
private void consume() throws InterruptedException {
// 使用 buffer 对象作为锁
synchronized (buffer) {
// 检查缓冲区是否为空,如果空了就等待
while (buffer.isEmpty()) {
System.out.println("Buffer is empty, Consumer is waiting...");
buffer.wait(); // 释放 buffer 的锁,并进入等待
}
int item = buffer.poll();
System.out.println("Consumer consumed: " + item);
// 通知可能在等待的生产者
buffer.notifyAll();
}
// 模拟消费耗时
Thread.sleep(1500);
}
}
}
代码解析:
- 共享锁:
buffer对象被用作所有线程共享的锁。 - 生产者逻辑:
- 在
synchronized块中获取buffer的锁。 - 使用
while (buffer.size() == CAPACITY)检查缓冲区是否已满。 - 如果满了,调用
buffer.wait()释放锁并等待。 - 当被唤醒后,
while循环再次检查条件,防止虚假唤醒。 - 生产数据后,调用
buffer.notifyAll()唤醒消费者。
- 在
- 消费者逻辑:
- 与生产者类似,但检查条件是
while (buffer.isEmpty())。 - 如果为空,调用
buffer.wait()等待。 - 消费数据后,调用
buffer.notifyAll()唤醒生产者。
- 与生产者类似,但检查条件是
wait() vs sleep() vs yield()
这是一个常见的面试题,三者的区别非常重要。
| 特性 | wait() |
sleep() |
yield() |
|---|---|---|---|
| 所属类 | Object |
Thread |
Thread |
| 锁行为 | 释放锁 | 不释放锁 | 不释放锁 |
| 使用位置 | 必须在 synchronized 代码块/方法中 |
可以在任何地方调用 | 可以在任何地方调用 |
| 唤醒方式 | 被 notify()/notifyAll() 唤醒 |
指定时间后自动唤醒 | 让出 CPU,调度器决定是否立即重新调度 |
| 目的 | 线程间协作,等待某个条件满足 | 让线程暂停一段时间,不占用 CPU | 提示调度器“我不忙,可以让其他线程运行” |
一个常见的误区:sleep() 不会释放锁,如果一个线程在 synchronized 代码块中调用了 Thread.sleep(1000),那么在这 1 秒内,其他任何线程都无法进入这个 synchronized 代码块,即使它们不需要锁。
现代替代方案:java.util.concurrent
虽然 wait/notify 是基础,但在现代 Java 开发中,更推荐使用 java.util.concurrent 包中的工具,它们更安全、更高效,也更容易使用。
BlockingQueue:这是一个接口,如ArrayBlockingQueue或LinkedBlockingQueue,它们内部已经实现了生产者-消费者的逻辑,你无需手动调用wait()和notify()。put(item):如果队列满,会自动阻塞,直到有空间。take():如果队列空,会自动阻塞,直到有元素。
Condition接口:这是Lock接口的一部分,提供了更灵活的等待/通知机制,可以创建多个等待队列,而wait/notify只能有一个。
使用 BlockingQueue 的生产者-消费者示例:
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
class ProducerConsumerModern {
private final BlockingQueue<Integer> queue;
public ProducerConsumerModern(int capacity) {
this.queue = new ArrayBlockingQueue<>(capacity);
}
public static void main(String[] args) {
ProducerConsumerModern example = new ProducerConsumerModern(5);
new Thread(example.new Producer()).start();
new Thread(example.new Consumer()).start();
}
class Producer implements Runnable {
private final Random random = new Random();
@Override
public void run() {
try {
while (true) {
int item = random.nextInt(100);
System.out.println("Producer produced: " + item);
queue.put(item); // 如果队列满,这里会自动阻塞
Thread.sleep(1000);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
class Consumer implements Runnable {
@Override
public void run() {
try {
while (true) {
int item = queue.take(); // 如果队列空,这里会自动阻塞
System.out.println("Consumer consumed: " + item);
Thread.sleep(1500);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
可以看到,代码简洁得多,无需手动管理锁和等待/通知逻辑。
- 核心用途:
wait()和notify()用于线程间的协作,一个线程需要等待另一个线程满足特定条件。 - 必须配合
synchronized使用:调用wait()的线程必须持有锁,notify()也必须在持有锁的线程中调用。 while循环是关键:永远不要在if语句中使用wait(),要用while循环来检查等待条件,以防止虚假唤醒和条件变化。notify()vsnotifyAll():notifyAll()通常更安全,可以避免线程饥饿。notify()在特定场景下(如只有一个消费者)性能可能更好,但风险更高。- 现代实践:在大多数情况下,优先使用
java.util.concurrent包中的高级工具(如BlockingQueue),它们封装了复杂的并发逻辑,使代码更健壮、更易读,理解wait/notify仍然是掌握 Java 并发的基础。
