深入理解wait/notify/notifyAll
本文讲解Java线程协作工具wait/notify,重点阐述了其必须在synchronized块和while循环中使用的核心规则。
我们来深入、详细地讲解一下Java中的wait()、notify()和notifyAll()。这三个方法是Java多线程协作的核心机制,但也是初学者容易出错的地方。
核心概念:监视器锁 (Monitor Lock)
在理解wait/notify之前,必须先理解Java的内置锁机制,也叫做监视器锁 (Monitor Lock)。
- 每个对象都有一个锁:在Java中,任何一个对象(
new Object())都可以作为一个锁。 synchronized关键字:当一个线程通过synchronized关键字获取了一个对象的锁之后,其他线程就无法再进入该对象上任何其他的synchronized代码块或方法,直到该线程释放锁。- 锁的归属:这个锁是属于对象的,而不是属于线程或代码块的。
wait()、notify()和notifyAll()就是围绕着这个对象监视器锁进行协作的工具。
wait(), notify(), notifyAll() 是什么?
这三个方法都定义在 java.lang.Object 类中。这意味着任何Java对象都可以调用这三个方法。
wait():- 当一个线程调用某个对象的
wait()方法时,它会立即释放该对象的锁。 - 然后,该线程进入等待状态 (WAITING),并被放入该对象的等待队列 (Wait Set)中。
- 它会一直等待,直到其他线程调用同一个对象的
notify()或notifyAll()方法。
- 当一个线程调用某个对象的
notify():- 当一个线程调用某个对象的
notify()方法时,它会从该对象的等待队列中随机唤醒一个正在等待的线程。 - 被唤醒的线程并不会立即执行,而是进入就绪状态 (RUNNABLE),需要重新竞争该对象的锁。只有当它成功获取到锁之后,才能从当初调用
wait()的地方继续执行。
- 当一个线程调用某个对象的
notifyAll():- 与
notify()类似,但它会唤醒该对象等待队列中所有正在等待的线程。 - 所有被唤醒的线程会一起去竞争该对象的锁。
- 与
使用 wait/notify 的三大黄金法则
这是最重要的部分,违反任何一条都会导致程序出错,通常是抛出 IllegalMonitorStateException。
法则一:必须在 synchronized 代码块或方法中使用
wait()、notify()、notifyAll() 的调用者必须是当前 synchronized 锁定的对象。
为什么?wait/notify 机制的本质是协调持有锁的线程和等待锁的线程。如果你都没有获取到对象的锁,凭什么去释放它(wait())或者通知其他等待该锁的线程(notify())呢?所以,调用这三个方法前,必须先通过 synchronized 获得该对象的监视器锁。
正确示例:
Object lock = new Object();
synchronized (lock) {
// 在这里调用 lock.wait() 或 lock.notify() 是合法的
lock.wait();
}
错误示例:
Object lock = new Object();
// 错误!当前线程没有持有 lock 对象的锁
lock.wait(); // 会抛出 IllegalMonitorStateException
法则二:调用 wait() / notify() 的对象必须与 synchronized 锁定的对象是同一个
这一点是法则一的延伸,但非常容易搞错。
正确示例:
public class MyService {
private Object lock = new Object();
public void doWait() {
synchronized (lock) { // 锁的是 lock 对象
try {
lock.wait(); // 在 lock 对象上等待,正确!
} catch (InterruptedException e) {
// ...
}
}
}
public void doNotify() {
synchronized (lock) { // 锁的是 lock 对象
lock.notify(); // 在 lock 对象上通知,正确!
}
}
}
错误示例:
public class MyService {
private Object lock = new Object();
public synchronized void doWait() { // 锁的是 this 对象
try {
// 错误!锁是this,却在lock对象上等待
lock.wait(); // 抛出 IllegalMonitorStateException
} catch (InterruptedException e) {
// ...
}
}
}
法则三:wait() 方法必须放在 while 循环中
为什么?
这主要是为了防止虚假唤醒 (Spurious Wakeup)。操作系统或JVM有时可能会在没有任何线程调用notify()或notifyAll()的情况下唤醒一个等待的线程。这虽然罕见,但确实可能发生。
如果使用 if,线程被虚假唤醒后会直接往下执行,此时它等待的条件可能并未满足,从而导致程序错误。而使用 while 循环,线程被唤醒后会重新检查等待的条件,如果条件不满足,它会再次调用 wait() 进入等待状态,从而保证了程序的健壮性。
正确示例(生产者-消费者模型中的消费者):
Queue<String> buffer = new LinkedList<>();
int capacity = 5;
// ...
synchronized (buffer) {
// 使用 while 循环检查条件
while (buffer.isEmpty()) {
try {
System.out.println("消费者等待中...");
buffer.wait(); // 缓冲区为空,等待
} catch (InterruptedException e) {
// ...
}
}
// 当循环结束,说明条件(buffer不为空)已经满足
String item = buffer.poll();
System.out.println("消费者消费了: " + item);
// ...
}
经典示例:生产者-消费者模型
这个例子最能体现 wait/notify 的作用。
import java.util.LinkedList;
import java.util.Queue;
public class ProducerConsumerExample {
public static void main(String[] args) {
Queue<Integer> buffer = new LinkedList<>();
int capacity = 5;
Thread producerThread = new Thread(new Producer(buffer, capacity), "Producer");
Thread consumerThread = new Thread(new Consumer(buffer), "Consumer");
producerThread.start();
consumerThread.start();
}
}
// 生产者
class Producer implements Runnable {
private final Queue<Integer> buffer;
private final int capacity;
public Producer(Queue<Integer> buffer, int capacity) {
this.buffer = buffer;
this.capacity = capacity;
}
@Override
public void run() {
int value = 0;
while (true) {
synchronized (buffer) {
// 使用 while 循环检查条件
while (buffer.size() == capacity) {
try {
System.out.println("缓冲区已满,生产者等待...");
buffer.wait(); // 释放锁,进入等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("生产者生产了: " + value);
buffer.add(value++);
// 唤醒可能在等待的消费者
buffer.notifyAll();
}
try {
// 模拟生产耗时
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 消费者
class Consumer implements Runnable {
private final Queue<Integer> buffer;
public Consumer(Queue<Integer> buffer) {
this.buffer = buffer;
}
@Override
public void run() {
while (true) {
synchronized (buffer) {
// 使用 while 循环检查条件
while (buffer.isEmpty()) {
try {
System.out.println("缓冲区为空,消费者等待...");
buffer.wait(); // 释放锁,进入等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int value = buffer.poll();
System.out.println("消费者消费了: " + value);
// 唤醒可能在等待的生产者
buffer.notifyAll();
}
try {
// 模拟消费耗时
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
为什么推荐使用 notifyAll() 而不是 notify()?
在上面的例子中,如果同时有多个生产者和多个消费者,notify() 可能会导致问题。
想象一下:
- 缓冲区满了,所有生产者都在
wait()。 - 一个消费者消费了一项,调用了
notify()。 - 这个
notify()不幸唤醒了另一个生产者。 - 这个被唤醒的生产者检查
while (buffer.size() == capacity),发现缓冲区还是满的(因为刚刚消费了一个,但可能马上又被其他生产者填满了,或者它自己醒来时还没空位),于是它又wait()了。 - 结果就是,一个本可以进行的消费操作没有被唤醒,造成了信号丢失,所有线程都可能永久等待下去,导致死锁。
而 notifyAll() 会唤醒所有等待的线程(包括所有生产者和所有消费者),它们都会去重新检查条件。虽然会带来一些不必要的竞争(“惊群效应”),但它能确保正确的线程(在这个例子中是消费者)最终会被唤醒并继续工作,程序逻辑是安全的。
与 Thread.sleep() 的区别
| 特性 | wait() |
Thread.sleep() |
|---|---|---|
| 所属类 | Object |
Thread |
| 是否释放锁 | 是,会释放持有的监视器锁 | 否,不会释放任何锁 |
| 唤醒方式 | 需要被其他线程通过 notify() 或 notifyAll() 唤醒 |
时间到了自动唤醒 |
| 使用场景 | 线程间的协作、通信 | 暂停当前线程的执行,不涉及协作 |
| 必要条件 | 必须在 synchronized 块或方法中调用 |
可以在任何地方调用 |
wait() 释放锁是它和 sleep() 最本质的区别,这使得其他线程有机会获取锁并改变条件,然后唤醒等待的线程。如果 wait() 不释放锁,那么其他线程将永远无法进入 synchronized 代码块,也就无法调用 notify(),造成死锁。
现代替代方案:Lock 和 Condition
在 java.util.concurrent.locks 包下,Java 提供了更强大、更灵活的锁机制。
Lock接口(如ReentrantLock)替代synchronized。Condition接口替代Object的wait/notify机制。
Condition 的优势:
- 多个等待队列:一个
Lock可以创建多个Condition对象。例如,在生产者-消费者模型中,可以创建一个 "缓冲区非满" 的条件和一个 "缓冲区非空" 的条件。生产者只在 "非满" 条件上等待,消费者只在 "非空" 条件上等待。当生产者生产后,只需唤醒等待 "非空" 条件的消费者,而不是所有线程,效率更高。 - 更丰富的功能:提供了
await(),signal(),signalAll(),分别对应wait(),notify(),notifyAll()。此外还支持可中断的等待、定时的等待等。
使用 Lock 和 Condition 的生产者-消费者示例:
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class ProducerConsumerWithLock {
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition(); // 生产者等待的条件
private final Condition notEmpty = lock.newCondition(); // 消费者等待的条件
private final Queue<Integer> buffer = new LinkedList<>();
private final int capacity = 5;
public void produce() throws InterruptedException {
int value = 0;
while (true) {
lock.lock(); // 获取锁
try {
while (buffer.size() == capacity) {
System.out.println("生产者等待...");
notFull.await(); // 在 notFull 条件上等待
}
buffer.add(value);
System.out.println("生产了: " + value++);
notEmpty.signalAll(); // 唤醒所有在 notEmpty 上等待的消费者
} finally {
lock.unlock(); // 必须在 finally 中释放锁
}
Thread.sleep(100);
}
}
public void consume() throws InterruptedException {
while (true) {
lock.lock();
try {
while (buffer.isEmpty()) {
System.out.println("消费者等待...");
notEmpty.await(); // 在 notEmpty 条件上等待
}
int value = buffer.poll();
System.out.println("消费了: " + value);
notFull.signalAll(); // 唤醒所有在 notFull 上等待的生产者
} finally {
lock.unlock();
}
Thread.sleep(500);
}
}
}
总结
wait/notify/notifyAll是Java中用于线程间通信和协作的底层机制。- 它们必须与
synchronized关键字配合使用,并且调用对象和锁对象必须是同一个。 wait()会释放锁并让线程进入等待状态。wait()必须放在while循环中以防止虚假唤醒。- 为避免死锁,优先使用
notifyAll()而不是notify()。 - 对于新的或复杂的并发场景,推荐使用
java.util.concurrent.locks包下的Lock和Condition,因为它们更灵活、功能更强大。