Java 中的 notify()
和 wait()
:线程间通信的关键机制
简介
在多线程编程中,线程间的有效通信至关重要。Java 提供了 notify()
和 wait()
方法来实现线程间的协作与同步。通过这两个方法,线程可以在特定条件下暂停执行(wait()
),并在条件满足时被唤醒(notify()
),从而实现复杂的并发控制逻辑。
目录
- 基础概念
notify()
方法wait()
方法- 监视器(Monitor)
- 使用方法
wait()
的调用notify()
的调用notifyAll()
的调用
- 常见实践
- 生产者 - 消费者模型
- 线程间的条件同步
- 最佳实践
- 避免死锁
- 合理使用
notify()
和notifyAll()
- 正确处理
InterruptedException
- 小结
- 参考资料
基础概念
notify()
方法
notify()
是 Object
类的一个方法,用于唤醒在此对象监视器上等待的单个线程。如果有多个线程在等待,则会随机选择一个线程被唤醒。被唤醒的线程将进入可运行状态,但并不一定会立即执行,具体执行时机由操作系统的线程调度器决定。
wait()
方法
wait()
同样是 Object
类的方法,它会使当前线程等待,直到其他线程调用该对象的 notify()
或 notifyAll()
方法。在等待期间,线程会释放对象的监视器,允许其他线程获取并访问该对象。这是实现线程同步和协作的关键机制。
监视器(Monitor)
在 Java 中,每个对象都有一个关联的监视器(也称为内置锁)。当一个线程访问被 synchronized
关键字修饰的代码块或方法时,它会自动获取对象的监视器。只有获取到监视器的线程才能执行相应的代码,其他线程则需要等待。wait()
、notify()
和 notifyAll()
方法的调用必须在获取对象监视器的前提下进行,否则会抛出 IllegalMonitorStateException
。
使用方法
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();
}
}
在上述代码中,waitingThread
线程进入 synchronized
块后调用 lock.wait()
,此时它会释放 lock
的监视器并进入等待状态。notifyingThread
线程在延迟 2 秒后启动,进入 synchronized
块并调用 lock.notify()
,唤醒 waitingThread
线程。waitingThread
被唤醒后会重新获取 lock
的监视器,继续执行后续代码。
notify()
的调用
如上述代码所示,notify()
方法通常在另一个线程中调用,用于唤醒等待在同一对象监视器上的单个线程。调用 notify()
时,必须确保当前线程已经获取了对象的监视器,否则会抛出异常。
notifyAll()
的调用
notifyAll()
方法会唤醒在此对象监视器上等待的所有线程。与 notify()
不同,notifyAll()
会让所有等待的线程都有机会竞争获取对象的监视器并继续执行。
public class NotifyAllExample {
public static void main(String[] args) {
Object lock = new Object();
Thread waitingThread1 = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("等待线程 1 开始等待...");
lock.wait();
System.out.println("等待线程 1 被唤醒...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread waitingThread2 = new Thread(() -> {
synchronized (lock) {
try {
System.out.println("等待线程 2 开始等待...");
lock.wait();
System.out.println("等待线程 2 被唤醒...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
waitingThread1.start();
waitingThread2.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Thread notifyingThread = new Thread(() -> {
synchronized (lock) {
System.out.println("通知线程开始通知所有线程...");
lock.notifyAll();
}
});
notifyingThread.start();
}
}
在这个例子中,waitingThread1
和 waitingThread2
都在等待 lock
的通知。notifyingThread
调用 lock.notifyAll()
后,两个等待线程都会被唤醒并竞争获取 lock
的监视器。
常见实践
生产者 - 消费者模型
生产者 - 消费者模型是多线程编程中的经典场景,通过 notify()
和 wait()
可以很方便地实现。生产者线程生成数据并放入缓冲区,消费者线程从缓冲区中取出数据进行处理。当缓冲区满时,生产者线程需要等待;当缓冲区为空时,消费者线程需要等待。
import java.util.LinkedList;
import java.util.Queue;
class ProducerConsumer {
private final Queue<Integer> buffer = new LinkedList<>();
private final int capacity = 5;
public void produce() throws InterruptedException {
while (true) {
synchronized (this) {
while (buffer.size() == capacity) {
System.out.println("缓冲区已满,生产者等待...");
wait();
}
int item = (int) (Math.random() * 100);
buffer.add(item);
System.out.println("生产者生产了: " + item);
notify();
}
}
}
public void consume() throws InterruptedException {
while (true) {
synchronized (this) {
while (buffer.isEmpty()) {
System.out.println("缓冲区为空,消费者等待...");
wait();
}
int item = buffer.poll();
System.out.println("消费者消费了: " + item);
notify();
}
}
}
}
public class ProducerConsumerExample {
public static void main(String[] args) {
ProducerConsumer pc = new ProducerConsumer();
Thread producerThread = new Thread(() -> {
try {
pc.produce();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread consumerThread = new Thread(() -> {
try {
pc.consume();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
producerThread.start();
consumerThread.start();
}
}
在上述代码中,ProducerConsumer
类包含一个缓冲区和 produce()
、consume()
方法。produce()
方法在缓冲区满时调用 wait()
等待,生产数据后调用 notify()
通知消费者;consume()
方法在缓冲区为空时调用 wait()
等待,消费数据后调用 notify()
通知生产者。
线程间的条件同步
有时候,线程需要在特定条件满足时才继续执行。通过 wait()
和 notify()
可以实现这种条件同步。
public class ConditionSyncExample {
private static boolean conditionMet = false;
public static void main(String[] args) {
Object lock = new Object();
Thread waitingThread = new Thread(() -> {
synchronized (lock) {
while (!conditionMet) {
try {
System.out.println("等待线程等待条件满足...");
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("等待线程条件满足,继续执行...");
}
});
Thread notifyingThread = new Thread(() -> {
synchronized (lock) {
System.out.println("通知线程设置条件并通知...");
conditionMet = true;
lock.notify();
}
});
waitingThread.start();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
notifyingThread.start();
}
}
在这个例子中,waitingThread
线程在 conditionMet
为 false
时调用 wait()
等待,notifyingThread
线程在延迟 2 秒后设置 conditionMet
为 true
并调用 notify()
唤醒 waitingThread
。
最佳实践
避免死锁
死锁是多线程编程中常见的问题,当两个或多个线程相互等待对方释放资源时就会发生死锁。为了避免死锁,应遵循以下原则:
- 尽量减少锁的嵌套使用。
- 确保线程获取锁的顺序一致。
- 使用 tryLock()
方法替代 lock()
方法,避免无限期等待。
合理使用 notify()
和 notifyAll()
notify()
适用于唤醒单个等待线程的场景,当只有一个线程需要被唤醒时,使用notify()
可以提高效率。notifyAll()
适用于多个等待线程都需要被唤醒的场景,例如在生产者 - 消费者模型中,当缓冲区状态发生变化时,可能需要唤醒所有等待的生产者或消费者线程。
正确处理 InterruptedException
wait()
方法会抛出 InterruptedException
,在调用 wait()
时必须正确处理该异常。通常情况下,应在捕获异常后清理资源并终止线程的执行。
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
// 清理资源
// 终止线程
Thread.currentThread().interrupt();
}
}
小结
notify()
和 wait()
是 Java 多线程编程中实现线程间通信和同步的重要方法。通过合理使用这两个方法,可以实现复杂的并发控制逻辑,如生产者 - 消费者模型和线程间的条件同步。在使用过程中,需要注意避免死锁、合理选择 notify()
和 notifyAll()
,并正确处理 InterruptedException
。掌握这些知识和技巧,将有助于编写高效、稳定的多线程程序。
参考资料
- 《Effective Java》,Joshua Bloch
- 《Java 并发编程实战》,Brian Goetz 等