1. 生产者消费者模型基础解析
生产者消费者模型是多线程编程中最经典的并发协作模式之一。想象一下餐厅后厨的工作场景:厨师们(生产者)不断制作菜品,服务员们(消费者)持续将做好的菜品送到顾客桌上。这个模型中,最关键的是厨师和服务员之间需要通过一个"传菜窗口"(缓冲区)进行协调,避免厨师做好的菜无处可放,或者服务员无菜可取的尴尬局面。
在Java中,我们使用Object类的wait()、notify()和notifyAll()方法来实现这种线程间协作。这三个方法都必须放在synchronized同步代码块或同步方法中调用,因为它们都需要先获得对象的监视器锁(monitor lock)。
关键理解:wait()会使当前线程释放锁并进入等待状态,notify()/notifyAll()则会唤醒等待该对象锁的线程。但被唤醒的线程不会立即执行,必须等到当前线程退出同步块释放锁后才会真正获得执行机会。
2. wait/notify机制深度剖析
2.1 方法语义与使用规范
wait()方法有三个重要特性:
- 调用后立即释放对象锁
- 线程状态转为WAITING
- 被唤醒后需要重新获取锁才能继续执行
notify()和notifyAll()的区别在于:
- notify()随机唤醒一个等待线程
- notifyAll()唤醒所有等待该对象锁的线程
// 典型使用模式 synchronized(lock) { while(条件不满足) { // 必须用while而不是if lock.wait(); } // 执行操作 lock.notifyAll(); // 或notify() }2.2 为什么必须使用while循环检查条件
初学者常犯的错误是用if代替while检查条件。考虑以下场景:
- 缓冲区空,消费者A和B都进入等待
- 生产者放入一个数据后调用notifyAll()
- 消费者A被唤醒,取走数据
- 消费者B也被唤醒,但此时缓冲区已空,如果使用if就会导致空指针异常
// 错误示范 synchronized(lock) { if(buffer.isEmpty()) { // 应该用while lock.wait(); } buffer.remove(); }3. 四种典型实现场景
3.1 单生产者单消费者模型
这是最简单的场景,不需要考虑线程间的复杂竞争。核心要点:
- 生产者检查缓冲区满则wait()
- 消费者检查缓冲区空则wait()
- 每次操作后调用notify()即可
// 生产者线程 public void run() { synchronized(lock) { while(buffer.isFull()) { lock.wait(); } buffer.put(item); lock.notify(); // 唤醒消费者 } } // 消费者线程 public void run() { synchronized(lock) { while(buffer.isEmpty()) { lock.wait(); } item = buffer.take(); lock.notify(); // 唤醒生产者 } }3.2 单生产者多消费者模型
当消费者线程多于一个时,必须注意:
- 必须使用while循环检查条件
- 应该使用notifyAll()而非notify()
- 考虑性能时可以优化为:生产者唤醒所有消费者,消费者只唤醒生产者
// 消费者改进版 public void run() { synchronized(lock) { while(buffer.isEmpty()) { // 必须用while lock.wait(); } item = buffer.take(); lock.notifyAll(); // 必须唤醒所有线程 } }3.3 多生产者单消费者模型
与单生产者多消费者对称,需要注意:
- 生产者之间也需要协调
- 生产者应该唤醒所有线程(包括其他生产者和消费者)
- 缓冲区操作需要更严格的同步控制
3.4 多生产者多消费者模型
这是最复杂的场景,完整实现需要考虑:
- 所有条件检查必须使用while
- 所有唤醒必须使用notifyAll()
- 建议使用单独的锁对象而非缓冲区本身
- 考虑使用更高级的并发容器如BlockingQueue
class Buffer { private final Queue<Integer> queue = new LinkedList<>(); private final int CAPACITY = 10; private final Object lock = new Object(); public void put(int item) throws InterruptedException { synchronized(lock) { while(queue.size() == CAPACITY) { lock.wait(); } queue.add(item); lock.notifyAll(); } } public int take() throws InterruptedException { synchronized(lock) { while(queue.isEmpty()) { lock.wait(); } int item = queue.remove(); lock.notifyAll(); return item; } } }4. 常见问题与性能优化
4.1 死锁场景分析
典型死锁情况:
- 所有生产者wait(),消费者也wait()
- 错误使用notify()导致关键线程未被唤醒
- 锁的嵌套使用不当
解决方案:
- 确保每次操作后都有唤醒调用
- 优先使用notifyAll()
- 避免在持有锁时调用外部方法
4.2 性能优化技巧
- 双重检查锁定:在wait()前增加一次条件检查
- 分离通知:生产者只唤醒消费者,消费者只唤醒生产者
- 使用Condition对象替代内置锁(更精细的等待/通知控制)
- 设置合理的超时时间:wait(long timeout)避免永久等待
// 使用Condition的优化实现 class OptimizedBuffer { private final Lock lock = new ReentrantLock(); private final Condition notFull = lock.newCondition(); private final Condition notEmpty = lock.newCondition(); public void put(int item) throws InterruptedException { lock.lock(); try { while(queue.size() == CAPACITY) { notFull.await(); } queue.add(item); notEmpty.signal(); // 只唤醒消费者 } finally { lock.unlock(); } } }4.3 监控与调试技巧
- 线程转储分析:jstack查看线程状态
- 日志记录:在关键操作前后添加日志
- 使用可视化工具:如JConsole、VisualVM
- 单元测试:模拟高并发场景
实战经验:在复杂场景中,建议先用简单的notifyAll()实现功能,再逐步优化为更精细的通知机制。过早优化往往是并发bug的根源。
5. 现代Java中的替代方案
虽然wait/notify是基础,但现代Java提供了更高级的替代方案:
- BlockingQueue接口:内置线程安全的阻塞队列
- Executor框架:封装了线程池和任务队列
- CompletableFuture:支持异步编程
- Flow API:响应式流处理
// 使用BlockingQueue的简化实现 BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10); // 生产者 queue.put(item); // 自动阻塞 // 消费者 Integer item = queue.take(); // 自动阻塞这些高级API底层仍然基于wait/notify机制,但提供了更安全易用的抽象。理解wait/notify的原理,有助于我们更好地使用这些高级工具,也能在需要定制特殊功能时游刃有余。