这两个方法是 Java 中实现线程间通信和协作的核心,属于 Object 类的方法,而不是 Thread 类,这是非常重要的一个点。

为什么需要 wait() 和 notify()?
在多线程编程中,我们经常遇到这样的场景:一个线程需要等待另一个线程完成某个任务后,才能继续执行。
-
不使用
wait/notify的问题:如果使用一个简单的while循环来轮询(不断检查)某个条件,会导致 CPU 空转,浪费大量计算资源。// 糟糕的实现方式 while (!condition) { // 什么也不做,只是空转 } // 当 condition 变为 true 时,继续执行这种方式极其低效,因为它占用了 CPU 时间片,却没有做任何有意义的工作。
-
wait/notify的解决方案:wait()和notify()提供了一种高效的机制,让等待的线程进入阻塞(Blocked)状态,释放 CPU 资源,当条件满足时,由另一个线程将其唤醒,使其从阻塞状态返回,继续执行。
(图片来源网络,侵删)
wait() 方法
当一个线程调用某个对象的 wait() 方法时,它会做以下几件事:
- 释放当前对象的锁:这是最关键的一点,调用
wait()的线程必须已经获得了该对象的锁(即它必须在同步代码块或同步方法中调用)。 - 进入该对象的等待队列:线程会进入该对象的
wait set,并立即释放锁。 - 进入阻塞状态:线程会一直等待,直到其他线程调用该对象的
notify()或notifyAll()方法来唤醒它。 - 被唤醒后重新获取锁:当线程被唤醒后,它会尝试重新获取之前释放的锁,一旦成功获取锁,它才会从
wait()方法调用处继续执行。
wait() 的重载方法
void wait(): 无限等待,直到被notify()或notifyAll()唤醒。void wait(long timeout): 等待指定的毫秒数,如果超时了,线程会自动被唤醒(抛出InterruptedException也会被唤醒)。void wait(long timeout, int nanos): 等待指定的毫秒和纳秒数。
重要:被唤醒的线程并不代表条件一定满足了!它只是获得了重新运行的机会,被唤醒的线程必须在一个 while 循环中再次检查条件,这被称为“伪唤醒”(Spurious Wakeup),虽然不常见,但 JVM 规范允许其发生。
notify() 和 notifyAll() 方法
这两个方法由持有锁的线程调用,用于唤醒正在等待的线程。
notify()
- 作用:随机唤醒一个正在等待该对象锁的线程。
- 选择哪个线程:这个选择是不固定的,由 JVM 决定,你无法指定唤醒哪个特定的线程。
- 场景:当你确定只有一个线程在等待,或者你“不关心”具体唤醒哪个线程时使用。
notifyAll()
- 作用:唤醒所有正在等待该对象锁的线程。
- 后续执行:所有被唤醒的线程都会去竞争对象的锁,只有一个线程能竞争成功并继续执行,其他的线程则再次进入阻塞状态,等待下一次获取锁。
- 场景:当你有多个线程在等待,并且需要根据某个具体条件来决定哪个线程应该继续执行时,
notifyAll()是更安全的选择,它通过让所有线程都去检查条件,来确保逻辑的正确性。
使用 wait/notify 的核心原则(必须遵守)
wait() 和 notify() 必须在同步代码块或同步方法中使用,并且必须使用同一个对象锁。
以下是必须遵守的三个核心原则:
- 必须在同步块/方法中调用:调用
wait()和notify()的线程必须先获取到该对象的锁。 - 必须使用同一个锁对象:
wait(),notify(),notifyAll()必须作用于同一个对象上。 - 永远在
while循环中调用wait():不要使用if语句,因为存在“伪唤醒”的可能,while循环可以确保在条件不满足时,线程会再次调用wait(),继续等待。
经典示例:生产者-消费者模型
这个模型是 wait/notify 最经典的应用场景。
import java.util.LinkedList;
import java.util.Queue;
class ProducerConsumerExample {
// 共享资源,使用同一个锁对象
private final Queue<Integer> queue = new LinkedList<>();
private final int MAX_SIZE = 5;
// 生产者
class Producer implements Runnable {
@Override
public void run() {
while (true) {
synchronized (queue) {
// 如果队列满了,生产者等待
while (queue.size() == MAX_SIZE) {
System.out.println("队列已满,生产者等待...");
try {
queue.wait(); // 释放 queue 的锁,并进入等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 队列未满,生产数据
int value = (int) (Math.random() * 100);
queue.add(value);
System.out.println("生产: " + value + " | 队列大小: " + queue.size());
// 通知消费者(可能有多个消费者在等待,所以用 notifyAll)
queue.notifyAll(); // 唤醒在 queue 上等待的线程
}
try {
Thread.sleep(1000); // 模拟生产耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 消费者
class Consumer implements Runnable {
@Override
public void run() {
while (true) {
synchronized (queue) {
// 如果队列空了,消费者等待
while (queue.isEmpty()) {
System.out.println("队列为空,消费者等待...");
try {
queue.wait(); // 释放 queue 的锁,并进入等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 队列非空,消费数据
int value = queue.poll();
System.out.println("消费: " + value + " | 队列大小: " + queue.size());
// 通知生产者(生产者可能在等待)
queue.notifyAll(); // 唤醒在 queue 上等待的线程
}
try {
Thread.sleep(1500); // 模拟消费耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public static void main(String[] args) {
ProducerConsumerExample example = new ProducerConsumerExample();
Thread producerThread = new Thread(example.new Producer());
Thread consumerThread = new Thread(example.new Consumer());
producerThread.start();
consumerThread.start();
}
}
示例解析:
- 共享资源:
queue是生产者和消费者共享的队列,MAX_SIZE是队列的最大容量。 - 锁对象:
synchronized (queue),所有对queue的操作都使用queue对象作为锁。 - 生产者逻辑:
- 在同步块中,先检查
while (queue.size() == MAX_SIZE)。 - 如果满了,调用
queue.wait()释放锁并等待。 - 如果没满,就生产数据并添加到队列。
- 调用
queue.notifyAll()唤醒可能在等待的消费者。
- 在同步块中,先检查
- 消费者逻辑:
- 在同步块中,先检查
while (queue.isEmpty())。 - 如果空了,调用
queue.wait()释放锁并等待。 - 如果不空,就从队列中取出数据消费。
- 调用
queue.notifyAll()唤醒可能在等待的生产者。
- 在同步块中,先检查
- 为什么用
notifyAll()而不是notify()?- 假设有两个消费者线程都在等待,如果生产者调用
notify(),它只能随机唤醒一个消费者,另一个消费者会继续等待,即使生产者已经生产了新数据,而notifyAll()会唤醒所有消费者,让它们去竞争锁,确保消费能顺利进行,在更复杂的场景下,notifyAll()能避免因唤醒错误的线程而导致的死锁或逻辑错误。
- 假设有两个消费者线程都在等待,如果生产者调用
wait/notify vs. java.util.concurrent
虽然 wait/notify 是 Java 线程通信的基础,但在现代 Java 开发中,我们更推荐使用 java.util.concurrent 包中的高级工具,因为它们
