生产者-消费者模式下的wait/notify深度解析
📖 目录导读

多线程协作的痛点
在现代软件开发中,多线程编程是提升性能的利器,但线程间的协调问题常常让开发者陷入困境,想象一个场景:一个线程不断生产数据,另一个线程需要及时消费这些数据,如果生产速度过快,数据会堆积;消费速度过快,则会出现空等待,如何让两个线程“心有灵犀”地协作?
经典问题:两个线程如何共享一个有限容量的缓冲区,既不丢失数据,也不重复消费?这正是生产者-消费者问题的核心。
生产者-消费者模式概述
生产者-消费者模式是一个经典并发设计模式,它通过一个共享缓冲区解耦生产者和消费者。
- 生产者:负责生成数据并放入缓冲区
- 消费者:负责从缓冲区取出数据进行处理
- 缓冲区:通常是有界队列,用于临时存储和流量控制
模式的价值
- 解耦:生产者和消费者不直接依赖
- 异步:生产与消费可不同步进行
- 削峰填谷:缓冲器能平滑数据流
问题:如何确保缓冲区满时生产者等待,缓冲区空时消费者等待?这正是wait/notify的用武之地。
wait/notify机制的核心原理
Java中的wait()、notify()和notifyAll()是Object类提供的线程通信方法,它们必须在同步块(synchronized)内使用。
工作原理
- wait():当前线程释放锁并进入等待状态,直到其他线程调用notify/notifyAll
- notify():随机唤醒一个在同一个锁对象上等待的线程
- notifyAll():唤醒所有在同一个锁对象上等待的线程
关键规则
- 调用wait/notify前必须持有对象的monitor锁
- wait会释放锁,notify不会释放锁
- 被唤醒的线程需要重新竞争锁
完整代码案例与逐行解析
下面是一个基于Java的生产者-消费者案例,使用wait/notify实现线程协作。
import java.util.LinkedList;
import java.util.Queue;
public class ProducerConsumerExample {
// 共享缓冲区:最大容量为5
private static final int CAPACITY = 5;
private final Queue<Integer> buffer = new LinkedList<>();
private final Object lock = new Object();
// 生产者
class Producer implements Runnable {
@Override
public void run() {
int value = 0;
while (true) {
synchronized (lock) {
// 缓冲区满时等待
while (buffer.size() == CAPACITY) {
System.out.println("缓冲区已满,生产者等待...");
try {
lock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
// 生产数据
buffer.offer(value);
System.out.println("生产者生产: " + value + " 当前缓冲区: " + buffer.size());
value++;
// 通知消费者消费
lock.notifyAll();
}
// 模拟生产耗时
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}
}
// 消费者
class Consumer implements Runnable {
@Override
public void run() {
while (true) {
synchronized (lock) {
// 缓冲区空时等待
while (buffer.isEmpty()) {
System.out.println("缓冲区为空,消费者等待...");
try {
lock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
// 消费数据
int value = buffer.poll();
System.out.println("消费者消费: " + value + " 当前缓冲区: " + buffer.size());
// 通知生产者生产
lock.notifyAll();
}
// 模拟消费耗时
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}
}
public static void main(String[] args) {
ProducerConsumerExample example = new ProducerConsumerExample();
Thread producerThread = new Thread(example.new Producer(), "生产者线程");
Thread consumerThread = new Thread(example.new Consumer(), "消费者线程");
producerThread.start();
consumerThread.start();
}
}
关键代码解读
- wait放在while循环内:防止“虚假唤醒”(spurious wakeup),确保条件满足后继续执行
- 使用notifyAll而非notify:避免“信号丢失”,因为notify可能唤醒同类型线程导致死锁
- 共享锁对象:生产者和消费者使用同一个lock对象,确保互斥和协作
运行结果示例
生产者生产: 0 当前缓冲区: 1
消费者消费: 0 当前缓冲区: 0
缓冲区为空,消费者等待...
生产者生产: 1 当前缓冲区: 1
生产者生产: 2 当前缓冲区: 2
...
缓冲区已满,生产者等待...
消费者消费: 4 当前缓冲区: 4
常见陷阱与性能优化
常见陷阱
- 死锁:忘记在wait前释放锁或使用错误锁对象
- 信号丢失:notify时没有检查条件,导致等待线程永远不被唤醒
- 嵌套同步:在同步块内调用其他同步方法可能导致死锁
性能优化技巧
- 使用ReentrantLock与Condition:提供更灵活的等待/通知机制,支持公平锁和多个条件队列
- 选择合适的数据结构:
LinkedBlockingQueue或ArrayBlockingQueue内置了阻塞机制,可替代手写wait/notify - 避免频繁notifyAll:大量线程竞争锁时,notifyAll会引发“惊群效应”,降低性能
优化后的代码(使用BlockingQueue)
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public class OptimizedProducerConsumer {
private static final int CAPACITY = 5;
private final BlockingQueue<Integer> buffer = new ArrayBlockingQueue<>(CAPACITY);
class Producer implements Runnable {
@Override
public void run() {
int value = 0;
try {
while (true) {
buffer.put(value);
System.out.println("生产者生产: " + value++);
Thread.sleep(500);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
class Consumer implements Runnable {
@Override
public void run() {
try {
while (true) {
int value = buffer.take();
System.out.println("消费者消费: " + value);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public static void main(String[] args) {
OptimizedProducerConsumer example = new OptimizedProducerConsumer();
new Thread(example.new Producer()).start();
new Thread(example.new Consumer()).start();
}
}
优势:BlockingQueue内置了lock和condition,无需手动管理wait/notify,代码更简洁、更安全。
问答环节:高频面试题与实战疑点
Q1: wait()和sleep()有什么区别?
A:
- 所属类:wait是Object方法,sleep是Thread静态方法
- 锁释放:wait释放锁,sleep不释放锁
- 唤醒方式:wait需要notify/notifyAll唤醒,sleep到时间自动唤醒
- 使用场景:wait用于线程通信,sleep用于暂停执行
Q2: 为什么wait/notify必须在synchronized块内使用?
A:为了确保线程安全,如果不加锁,在检查条件(如buffer.isEmpty())和调用wait之间,其他线程可能修改状态,导致条件判断失效,synchronized保证原子性和可见性。
Q3: 为什么用while循环检查条件,而不是if?
A:防止虚假唤醒和信号丢失,Java文档允许线程在没有notify的情况下被唤醒,while循环确保条件重新检查,避免“过早唤醒”导致的错误。
Q4: 多个生产者和消费者应该用notify还是notifyAll?
A:建议使用notifyAll,notify可能唤醒同类型线程(如两个生产者),导致另一个类型线程永远不被唤醒(信号丢失),notifyAll唤醒所有等待线程,确保系统正确运行,但可能引发轻微的性能开销。
Q5: wait/notify和Lock+Condition如何选择?
A:
- 简单场景:wait/notify足够,但需小心陷阱
- 复杂场景:Lock+Condition更灵活,支持公平锁、多个条件队列、超时等待
- 生产环境:优先使用BlockingQueue实现,避免手写线程通信
总结与最佳实践
生产者-消费者模式通过wait/notify实现了线程间的优雅协作,关键要点包括:
- 始终在同步块内使用wait/notify
- 使用while循环检查条件,防止虚假唤醒
- 优先使用notifyAll,避免信号丢失
- 考虑使用BlockingQueue,简化线程协调
- 注意中断处理,调用wait后需处理InterruptedException
最佳实践速查表
| 场景 | 推荐方案 |
|---|---|
| 一对生产者-消费者 | wait/notify或BlockingQueue |
| 多对多 | Condition或BlockingQueue |
| 高并发 | Lock+公平锁+Condition |
| 快速原型 | BlockingQueue |
线程协作的艺术在于平衡并发与安全,掌握wait/notify是理解高级并发工具的基础,但生产环境中应优先选择经过验证的并发集合类,一个好的生产者-消费者实现应该像一场完美的交响乐:每个线程各司其职,在合适的时机演奏(工作),在无声处等待(等待),共同奏出高效协调的乐章。