使用 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
,以提高代码的可读性和可靠性。