一、wait/notify机制的基本原理
wait/notify是Java多线程编程中最基础的线程间通信机制,它们都是Object类的方法而非Thread类的方法,这意味着任何Java对象都可以作为线程间通信的媒介。这套机制的核心思想是通过对象监视器(Monitor)实现线程的等待与唤醒。
基本工作原理:
- wait():使当前线程进入等待状态,并释放对象锁
- notify():随机唤醒一个在该对象上等待的线程
- notifyAll():唤醒所有在该对象上等待的线程
这三个方法必须配合synchronized使用,因为它们依赖于对象的内置锁(监视器锁)。调用这些方法时,当前线程必须持有该对象的锁,否则会抛出IllegalMonitorStateException异常。
二、wait/notify的核心方法详解
1. wait方法系列
wait()
:使当前线程等待,直到其他线程调用此对象的notify()或notifyAll()方法wait(long timeout)
:带超时时间的等待,单位毫秒wait(long timeout, int nanos)
:更精确的超时控制,可指定纳秒
2. notify与notifyAll
notify()
:唤醒在此对象监视器上等待的单个线程。如果有多个线程在等待,则选择其中一个进行唤醒(选择是任意的)notifyAll()
:唤醒在此对象监视器上等待的所有线程
关键区别:
- notify()仅唤醒一个线程,可能导致线程饥饿问题
- notifyAll()唤醒所有线程,确保不会有线程被遗漏,但会带来更大的性能开销
三、wait/notify的正确使用方式
1. 基本使用规则
- 必须在synchronized同步块或方法中调用
- 调用wait/notify的对象必须与synchronized锁定的对象相同
- wait后必须使用循环检查等待条件(避免虚假唤醒)
2. 避免虚假唤醒
Java官方文档明确指出:"线程可能在没有被通知、中断或超时的情况下被唤醒,这被称为虚假唤醒"。因此,等待应该总是发生在循环中。
正确写法:
csharp
synchronized (lock) {
while (!condition) { // 使用while而非if
lock.wait();
}
// 条件满足后的处理
}
3. 锁的释放与获取
- 调用wait()会立即释放锁,使其他线程可以获取锁
- 调用notify()/notifyAll()不会立即释放锁,只有在同步块执行完毕后才会释放
- 被唤醒的线程需要重新竞争锁才能继续执行
四、经典应用场景与实战示例
1. 生产者-消费者模式
这是wait/notify最典型的应用场景,解决生产者和消费者速度不匹配的问题。
基础实现:
arduino
public class WaitNotifyExample {
private final Queue<String> queue = new LinkedList<>();
private final int MAX_SIZE = 5;
public synchronized void produce(String data) throws InterruptedException {
while (queue.size() == MAX_SIZE) {
wait(); // 队列满,生产者等待
}
queue.add(data);
notify(); // 唤醒消费者
}
public synchronized String consume() throws InterruptedException {
while (queue.isEmpty()) {
wait(); // 队列空,消费者等待
}
String data = queue.poll();
notify(); // 唤醒生产者
return data;
}
}
多生产者多消费者场景:
在多生产者多消费者场景下,应改用notifyAll()避免线程饥饿:
arduino
public synchronized void produceMulti(String data) throws InterruptedException {
while (queue.size() == MAX_SIZE) {
wait();
}
queue.add(data);
notifyAll(); // 必须用notifyAll确保正确唤醒
}
2. 线程交替执行
wait/notify可用于控制线程按特定顺序交替执行,如交替打印数字和字母:
csharp
public class AlternatePrint {
private final Object lock = new Object();
private boolean flag = true; // 控制交替的标志
public void printNum() throws InterruptedException {
synchronized (lock) {
for (int i = 1; i <= 26; i++) {
while (!flag) {
lock.wait();
}
System.out.print(i);
flag = false;
lock.notify();
}
}
}
public void printChar() throws InterruptedException {
synchronized (lock) {
for (char c = 'A'; c <= 'Z'; c++) {
while (flag) {
lock.wait();
}
System.out.print(c + " ");
flag = true;
lock.notify();
}
}
}
}
五、wait/notify的高级应用与注意事项
1. 通知过早问题
如果notify()在wait()之前调用,可能导致等待线程永远无法被唤醒。解决方案是添加状态标志:
csharp
public class EarlyNotifyExample {
static boolean isFirst = true; // 是否第一个运行的线程标志
public static void main(String[] args) {
final Object lock = new Object();
Thread t1 = new Thread(() -> {
synchronized (lock) {
while (isFirst) {
try {
System.out.println("wait begin");
lock.wait();
System.out.println("wait end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock) {
System.out.println("notify begin");
lock.notify();
System.out.println("notify end");
isFirst = false;
}
});
t1.start();
t2.start();
}
}
2. wait等待条件变化
当多个线程等待同一条件时,条件变化后应确保所有相关线程被正确通知:
csharp
public class ConditionChangeExample {
static List list = new ArrayList<>();
static public void subtract() {
synchronized (list) {
while (list.size() == 0) {
try {
System.out.println(Thread.currentThread().getName()+" wait begin");
list.wait();
System.out.println(Thread.currentThread().getName()+" wait end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
list.remove(0);
}
}
}
3. 死锁预防
使用带超时的wait(long timeout)可以避免永久等待导致的死锁:
kotlin
// 超时等待,避免永久死锁
if (!condition) {
this.wait(1000); // 最多等待1秒
}
六、wait/notify与Condition的对比
Java 5引入了Condition接口,提供了比wait/notify更灵活的线程间控制机制:
特性 | wait/notify | Condition |
---|---|---|
关联锁 | 必须与synchronized配合使用 | 必须与Lock配合使用 |
等待队列 | 一个对象一个等待队列 | 一个Lock可创建多个Condition |
精确通知 | 不支持 | 支持分组唤醒 |
超时控制 | 有限支持 | 更灵活的超时机制 |
中断响应 | 只能抛出InterruptedException | 提供可中断和不可中断的等待 |
Condition优势场景:
- 需要精确唤醒特定类型线程(如只唤醒生产者或消费者)
- 需要更灵活的超时控制
- 需要实现公平锁机制
七、最佳实践与常见陷阱
1. 最佳实践
- 始终在循环中调用wait():防止虚假唤醒
- 优先使用notifyAll():除非能确保notify()不会导致线程饥饿
- 保持同步块简短:减少锁持有时间,提高并发性
- 正确处理中断:恢复中断状态,不吞掉中断
2. 常见陷阱
- 在非同步块中调用wait/notify:导致IllegalMonitorStateException
- 使用不同对象的wait/notify:无法实现线程间通信
- 忽略虚假唤醒:使用if而非while检查条件
- notify过早:在wait之前调用notify导致信号丢失
- 过度使用notifyAll:在明确知道只需唤醒一个线程时使用notifyAll会造成性能浪费
八、性能考量与替代方案
1. 性能优化
- 减小同步范围:只同步必要的代码块
- 分离读写锁:读多写少场景使用ReadWriteLock
- 使用并发集合:如ConcurrentHashMap替代同步的HashMap
2. 高级替代方案
- CountDownLatch:主线程等待多个子任务完成
- CyclicBarrier:线程分阶段协同工作
- CompletableFuture:Java 8引入的异步任务编排
- BlockingQueue:内置阻塞机制的线程安全队列
wait/notify作为Java最基础的线程间通信机制,理解其原理和正确使用方式对于编写可靠的多线程程序至关重要。虽然Java并发包提供了更多高级同步工具,但在许多场景下,wait/notify仍然是简单有效的解决方案。