使用 wait、notify、notifyAll 方法时需要注意哪些问题?
参考回答**
在使用 wait、notify 和 notifyAll 方法时,需要注意以下问题:
- 只能在同步代码块或同步方法中使用:
- 这三个方法必须在获取了对象锁的前提下调用,否则会抛出
IllegalMonitorStateException异常。 - 它们必须在
synchronized修饰的代码块或方法内使用。
- 这三个方法必须在获取了对象锁的前提下调用,否则会抛出
- 必须配合对象锁使用:
- 这些方法是属于对象实例的,而非线程。调用
wait和notify时,线程必须持有调用对象的锁。
- 这些方法是属于对象实例的,而非线程。调用
wait方法会释放锁:- 当线程调用
wait时,会释放当前持有的对象锁,并进入等待状态,直到被notify或notifyAll唤醒。 - 唤醒后,线程需要重新尝试获取锁,才能继续执行。
- 当线程调用
notify和notifyAll不释放锁:- 调用
notify或notifyAll的线程不会立即释放锁,而是继续执行代码,直到退出同步代码块后才释放锁。
- 调用
- 避免死锁和遗漏唤醒:
- 需要精心设计同步逻辑,避免多个线程之间互相等待而导致死锁。
- 如果使用
notify唤醒特定线程,可能会导致某些线程一直处于等待状态。
- 推荐使用
while而非if检查条件:- 在等待条件时,推荐使用
while循环重新检查条件,而不是if,以避免虚假唤醒(Spurious Wakeup)。
- 在等待条件时,推荐使用
详细讲解与拓展
1. 使用方法
wait方法:使当前线程进入等待状态,并释放当前持有的锁。synchronized (lock) { lock.wait(); // 当前线程等待 }notify方法:唤醒一个在wait状态下等待的线程。synchronized (lock) { lock.notify(); // 唤醒一个线程 }notifyAll方法:唤醒所有在wait状态下等待的线程。synchronized (lock) { lock.notifyAll(); // 唤醒所有线程 }
2. 示例代码
生产者-消费者模型:
import java.util.LinkedList;
import java.util.Queue;
public class ProducerConsumer {
private final Queue<Integer> queue = new LinkedList<>();
private final int CAPACITY = 5;
public static void main(String[] args) {
ProducerConsumer pc = new ProducerConsumer();
Thread producer = new Thread(pc.new Producer());
Thread consumer = new Thread(pc.new Consumer());
producer.start();
consumer.start();
}
class Producer implements Runnable {
@Override
public void run() {
int value = 0;
while (true) {
synchronized (queue) {
while (queue.size() == CAPACITY) {
try {
System.out.println("Queue is full, producer waiting...");
queue.wait(); // 释放锁并等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Producing: " + value);
queue.offer(value++);
queue.notifyAll(); // 唤醒消费者
}
}
}
}
class Consumer implements Runnable {
@Override
public void run() {
while (true) {
synchronized (queue) {
while (queue.isEmpty()) {
try {
System.out.println("Queue is empty, consumer waiting...");
queue.wait(); // 释放锁并等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
int value = queue.poll();
System.out.println("Consuming: " + value);
queue.notifyAll(); // 唤醒生产者
}
}
}
}
}
3. 常见问题和注意事项
- 必须持有锁:
- 调用
wait、notify或notifyAll方法时,当前线程必须持有对象锁,否则会抛出IllegalMonitorStateException。 -
例如:
“`java
Object lock = new Object();
lock.notify(); // 错误:没有持有锁
“`
- 使用
while检查条件:
-
避免虚假唤醒时条件判断失败。例如:
“`java
synchronized (lock) {
while (conditionNotMet) { // 用 while 循环而非 if
lock.wait();
}
// 继续执行逻辑
}
“`
- 避免死锁:
- 如果线程在进入
wait后没有合适的线程调用notify或notifyAll,会导致程序永久阻塞。
notify与notifyAll的选择:
notify唤醒一个等待线程;notifyAll唤醒所有等待线程。- 推荐在条件复杂时使用
notifyAll,以避免遗漏唤醒的风险。
- 配合条件变量使用:
- 在多线程场景下,可能存在多个条件变量需要分别处理。例如,生产者-消费者模型中可以用两个标志变量分别控制队列的空满状态。
4. 拓展知识
- 虚假唤醒(Spurious Wakeup)
- 虚假唤醒指线程在没有明确
notify或notifyAll调用的情况下被唤醒,这在多处理器环境中可能发生。 - 为避免问题,必须在
wait返回后重新检查条件。
wait的超时版本
-
wait方法支持超时参数,避免线程无限期等待:
“`java
synchronized (lock) {
lock.wait(1000); // 最多等待 1 秒
}
“`
- 推荐使用高级并发工具
- 虽然
wait和notify是线程间通信的基础,但更推荐使用 Java 的并发工具类(如Condition、Semaphore或BlockingQueue),它们封装了更高级的同步机制,易于使用且更安全。
总结
- 使用
wait、notify、notifyAll时必须确保在同步代码块内调用,并小心处理线程间的通信和状态条件。 - 推荐使用
while循环检查条件,以避免虚假唤醒带来的问题。 - 对于复杂场景,可以考虑使用更高级的并发工具类,如
Condition或BlockingQueue,以提高代码的可读性和可靠性。