Java 中的 Wait 和 Notify:线程间通信的关键机制
简介
在多线程编程中,线程间的有效通信至关重要。Java 提供了 wait()
和 notify()
方法,它们是实现线程间同步与通信的基础工具。通过这两个方法,线程可以暂停执行并等待特定条件满足,同时其他线程能够在条件达成时通知等待的线程继续执行。本文将深入探讨 wait()
和 notify()
的概念、使用方法、常见实践以及最佳实践,帮助读者更好地掌握这一重要的多线程编程技术。
目录
- 基础概念
wait()
方法notify()
方法notifyAll()
方法
- 使用方法
- 调用
wait()
- 调用
notify()
和notifyAll()
- 示例代码
- 调用
- 常见实践
- 生产者 - 消费者模式
- 资源池管理
- 最佳实践
- 避免死锁
- 使用
while
循环检查条件 - 合理选择
notify()
和notifyAll()
- 小结
- 参考资料
基础概念
wait()
方法
wait()
是 Object
类的实例方法。当一个线程调用对象的 wait()
方法时,该线程会释放对象的锁,并进入等待状态。直到其他线程调用该对象的 notify()
或 notifyAll()
方法,或者等待时间超时(如果使用了带超时参数的 wait()
方法),该线程才会被唤醒并重新尝试获取对象的锁,然后继续执行。
notify()
方法
notify()
同样是 Object
类的实例方法。当一个线程调用对象的 notify()
方法时,它会随机唤醒在此对象监视器(锁)上等待的单个线程。被唤醒的线程将进入可运行状态,等待获取对象的锁,一旦获取到锁,就会从 wait()
方法调用处继续执行。
notifyAll()
方法
notifyAll()
也是 Object
类的实例方法。与 notify()
不同,notifyAll()
会唤醒在此对象监视器(锁)上等待的所有线程。这些被唤醒的线程都会竞争获取对象的锁,获取到锁的线程将从 wait()
方法调用处继续执行。
使用方法
调用 wait()
要调用 wait()
方法,必须在同步块(synchronized
block)或同步方法(synchronized
method)中进行,因为 wait()
方法会释放对象的锁。示例如下:
public class WaitExample {
public static void main(String[] args) {
Object lock = new Object();
Thread waitingThread = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("等待线程开始等待...");
lock.wait();
System.out.println("等待线程被唤醒...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
waitingThread.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread notifyingThread = new Thread(() -> {
synchronized (lock) {
System.out.println("通知线程开始通知...");
lock.notify();
}
});
notifyingThread.start();
}
}
调用 notify()
和 notifyAll()
notify()
和 notifyAll()
方法也必须在同步块或同步方法中调用,因为它们需要获取对象的锁才能操作等待队列中的线程。在上述示例中,notifyingThread
调用了 lock.notify()
来唤醒等待线程。
示例代码
下面是一个完整的生产者 - 消费者示例,展示了 wait()
和 notify()
的实际应用:
import java.util.LinkedList;
import java.util.Queue;
class Producer implements Runnable {
private final Queue<Integer> queue;
private final int capacity;
public Producer(Queue<Integer> queue, int capacity) {
this.queue = queue;
this.capacity = capacity;
}
@Override
public void run() {
int value = 0;
while (true) {
synchronized (queue) {
while (queue.size() == capacity) {
try {
System.out.println("队列已满,生产者等待...");
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
queue.add(value++);
System.out.println("生产者生产: " + value);
queue.notify();
}
}
}
}
class Consumer implements Runnable {
private final Queue<Integer> queue;
public Consumer(Queue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
while (true) {
synchronized (queue) {
while (queue.isEmpty()) {
try {
System.out.println("队列为空,消费者等待...");
queue.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int consumed = queue.poll();
System.out.println("消费者消费: " + consumed);
queue.notify();
}
}
}
}
public class ProducerConsumerExample {
public static void main(String[] args) {
Queue<Integer> queue = new LinkedList<>();
int capacity = 5;
Thread producerThread = new Thread(new Producer(queue, capacity));
Thread consumerThread = new Thread(new Consumer(queue));
producerThread.start();
consumerThread.start();
}
}
常见实践
生产者 - 消费者模式
在生产者 - 消费者模式中,生产者线程生成数据并将其放入共享队列,消费者线程从队列中取出数据进行处理。wait()
和 notify()
方法用于协调生产者和消费者之间的同步,确保队列在满时生产者等待,队列为空时消费者等待。上述示例代码就是一个典型的生产者 - 消费者模式实现。
资源池管理
在资源池管理中,资源池维护一定数量的资源,多个线程需要获取和释放这些资源。当资源池为空时,请求资源的线程可以调用 wait()
方法等待资源可用;当有线程释放资源时,调用 notify()
或 notifyAll()
方法唤醒等待的线程。
最佳实践
避免死锁
死锁是多线程编程中常见的问题,使用 wait()
和 notify()
时需要特别注意。确保线程获取锁和释放锁的顺序一致,避免循环依赖导致死锁。例如,在多个线程对多个对象进行加锁操作时,统一按照某种固定顺序获取锁。
使用 while
循环检查条件
在调用 wait()
方法时,应该使用 while
循环检查等待条件,而不是 if
语句。这是因为线程被唤醒后可能不是因为等待的条件满足,而是被虚假唤醒。使用 while
循环可以确保线程在条件真正满足时才继续执行。
synchronized (lock) {
while (!condition) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 执行相应操作
}
合理选择 notify()
和 notifyAll()
如果只有一个线程等待特定条件,使用 notify()
方法可以提高效率,因为它只唤醒一个线程。但如果有多个线程等待不同条件,或者不确定具体等待的线程数量,使用 notifyAll()
方法确保所有等待线程都有机会被唤醒并检查条件。
小结
wait()
和 notify()
是 Java 多线程编程中实现线程间通信的重要方法。通过合理使用这两个方法,我们可以实现复杂的多线程同步逻辑,如生产者 - 消费者模式和资源池管理。在使用过程中,遵循最佳实践,如避免死锁、使用 while
循环检查条件以及合理选择唤醒方法,能够提高程序的稳定性和性能。掌握 wait()
和 notify()
的使用是成为一名优秀 Java 多线程开发者的关键一步。
参考资料
- Oracle Java 教程 - 线程间通信
- 《Effective Java》第 69 条:避免使用线程组
- 《Java 并发编程实战》第 3 章:对象的共享