如何通过 Object.wait() 和 notify() 实现多线程生产者-消费者模型协作?
- 内容介绍
- 文章标签
- 相关推荐
本文共计1088个文字,预计阅读时间需要5分钟。
因为这两个方法必须在同步上下文中执行——也就是说,当前线程必须保持对相应对象的监视器锁。否则,会抛出IllegalMonitorStateException。常见错误是忘记添加synchronized块,或者锁的对象和调用wait()的对象不一致。
正确做法是:所有对共享缓冲区的操作(读/写/等待)都必须在同一个锁对象上同步,通常就是缓冲区实例本身。
- 用
synchronized(buffer)包裹所有涉及wait()/notify()的逻辑 - 避免用
this或新创建的锁对象,否则生产者和消费者无法看到彼此的通知 -
wait()一定要放在while循环里判断条件,不能用if——防止虚假唤醒
如何用 wait()/notify() 实现带边界检查的阻塞队列
核心是把“缓冲区满”和“缓冲区空”作为两个独立等待条件,通过同一个锁对象协调。
示例中用一个 ArrayList 模拟固定容量队列,生产者在满时 wait(),消费者在空时 wait();任一操作完成后都调用 notifyAll()(不是 notify()),确保不会漏掉等待方。
public class BoundedBuffer { private final List<String> buffer = new ArrayList<>(); private final int capacity = 5; public void produce(String item) throws InterruptedException { synchronized (buffer) { while (buffer.size() == capacity) { buffer.wait(); // 等待有空间 } buffer.add(item); buffer.notifyAll(); // 唤醒可能等待取数据的消费者 } } public String consume() throws InterruptedException { synchronized (buffer) { while (buffer.isEmpty()) { buffer.wait(); // 等待有数据 } String item = buffer.remove(0); buffer.notifyAll(); // 唤醒可能等待放数据的生产者 return item; } } }
- 用
notifyAll()而非notify():避免唤醒错线程类型(比如只唤醒另一个生产者) - 容量检查必须用
while:JVM 可能无理由唤醒线程(spurious wakeup),不重检条件会出错 - 注意
remove(0)是 O(n) 操作,实际应改用LinkedList或数组循环队列
wait(long timeout) 怎么用才安全
带超时的 wait() 不等于“最多等 timeout”,而是“最多等 timeout,但可能提前被唤醒”。所以它不能替代条件判断,仍需配合 while 循环使用。
典型误用是写了 if (buffer.isEmpty()) buffer.wait(1000);,结果超时后直接取数据,没再检查是否真有数据。
- 超时后必须重新判断条件,否则可能触发
IndexOutOfBoundsException - 超时适合做“保底响应”,比如日志告警、降级返回 null,而不是跳过条件检查
- 不要依赖超时值做业务逻辑分流(如“等不到就走异步路径”),那应该用
Lock.tryLock()+ 条件队列
和 java.util.concurrent 里的实现比,手写有什么硬伤
手写 wait()/notify() 最大的问题是无法分离“等待条件”——所有 wait() 都共用一个等待队列,导致每次 notifyAll() 都要唤醒全部线程,再由它们各自重判条件。这在高并发下浪费严重。
ArrayBlockingQueue 内部用 ReentrantLock + 两个 Condition(notFull / notEmpty),可以精准唤醒对应等待组,避免无效竞争。
- 手写版在百级线程规模下,
notifyAll()引发的上下文切换开销会明显上升 - 没有中断支持:原生
wait()可被interrupt()中断,但需要手动处理InterruptedException并恢复中断状态 - 无法实现公平策略、无法绑定多个等待条件、难以调试死锁点
除非教学或嵌入式受限环境,否则优先用 BlockingQueue 实现。手写只是为了理解底层协作契约——wait 是释放锁并挂起,notify 是唤醒但不抢锁,真正竞争在重新进入同步块时才开始。
本文共计1088个文字,预计阅读时间需要5分钟。
因为这两个方法必须在同步上下文中执行——也就是说,当前线程必须保持对相应对象的监视器锁。否则,会抛出IllegalMonitorStateException。常见错误是忘记添加synchronized块,或者锁的对象和调用wait()的对象不一致。
正确做法是:所有对共享缓冲区的操作(读/写/等待)都必须在同一个锁对象上同步,通常就是缓冲区实例本身。
- 用
synchronized(buffer)包裹所有涉及wait()/notify()的逻辑 - 避免用
this或新创建的锁对象,否则生产者和消费者无法看到彼此的通知 -
wait()一定要放在while循环里判断条件,不能用if——防止虚假唤醒
如何用 wait()/notify() 实现带边界检查的阻塞队列
核心是把“缓冲区满”和“缓冲区空”作为两个独立等待条件,通过同一个锁对象协调。
示例中用一个 ArrayList 模拟固定容量队列,生产者在满时 wait(),消费者在空时 wait();任一操作完成后都调用 notifyAll()(不是 notify()),确保不会漏掉等待方。
public class BoundedBuffer { private final List<String> buffer = new ArrayList<>(); private final int capacity = 5; public void produce(String item) throws InterruptedException { synchronized (buffer) { while (buffer.size() == capacity) { buffer.wait(); // 等待有空间 } buffer.add(item); buffer.notifyAll(); // 唤醒可能等待取数据的消费者 } } public String consume() throws InterruptedException { synchronized (buffer) { while (buffer.isEmpty()) { buffer.wait(); // 等待有数据 } String item = buffer.remove(0); buffer.notifyAll(); // 唤醒可能等待放数据的生产者 return item; } } }
- 用
notifyAll()而非notify():避免唤醒错线程类型(比如只唤醒另一个生产者) - 容量检查必须用
while:JVM 可能无理由唤醒线程(spurious wakeup),不重检条件会出错 - 注意
remove(0)是 O(n) 操作,实际应改用LinkedList或数组循环队列
wait(long timeout) 怎么用才安全
带超时的 wait() 不等于“最多等 timeout”,而是“最多等 timeout,但可能提前被唤醒”。所以它不能替代条件判断,仍需配合 while 循环使用。
典型误用是写了 if (buffer.isEmpty()) buffer.wait(1000);,结果超时后直接取数据,没再检查是否真有数据。
- 超时后必须重新判断条件,否则可能触发
IndexOutOfBoundsException - 超时适合做“保底响应”,比如日志告警、降级返回 null,而不是跳过条件检查
- 不要依赖超时值做业务逻辑分流(如“等不到就走异步路径”),那应该用
Lock.tryLock()+ 条件队列
和 java.util.concurrent 里的实现比,手写有什么硬伤
手写 wait()/notify() 最大的问题是无法分离“等待条件”——所有 wait() 都共用一个等待队列,导致每次 notifyAll() 都要唤醒全部线程,再由它们各自重判条件。这在高并发下浪费严重。
ArrayBlockingQueue 内部用 ReentrantLock + 两个 Condition(notFull / notEmpty),可以精准唤醒对应等待组,避免无效竞争。
- 手写版在百级线程规模下,
notifyAll()引发的上下文切换开销会明显上升 - 没有中断支持:原生
wait()可被interrupt()中断,但需要手动处理InterruptedException并恢复中断状态 - 无法实现公平策略、无法绑定多个等待条件、难以调试死锁点
除非教学或嵌入式受限环境,否则优先用 BlockingQueue 实现。手写只是为了理解底层协作契约——wait 是释放锁并挂起,notify 是唤醒但不抢锁,真正竞争在重新进入同步块时才开始。

