最稳妥的方式是使用BlockingQueue而非手写wait/notify,因其天然线程安全、阻塞语义明确、边界处理完整;手写易出现唤醒丢失、虚假唤醒、未用while循环检查条件及锁粒度不合理等问题。

Java里实现生产者消费者模型,最稳妥的方式不是手写 wait/notify,而是用 BlockingQueue——它天然线程安全、阻塞语义明确、边界处理完整。
为什么别直接用 wait/notify 手写?
手写容易漏掉几个关键点:唤醒丢失(notify 早于 wait)、虚假唤醒(spurious wakeup)、未在循环中检查条件、锁粒度不合理。哪怕代码看着“能跑”,在高并发或压力下极易出现死锁、数据丢失或无限等待。
常见错误现象包括:
- 生产者一直阻塞,消费者取不到新数据(notify 被忽略)
- 队列为空时消费者仍尝试取值(没用 while 循环重检条件)
- 多个生产者/消费者共用同一把锁,吞吐量骤降
BlockingQueue 的三种典型用法
选哪种取决于场景对容量、公平性、响应性的要求:
立即学习“Java免费学习笔记(深入)”;
-
ArrayBlockingQueue:有界、基于数组、构造时必须指定容量;适合内存敏感、需硬性限流的场景(如日志缓冲区) -
LinkedBlockingQueue:默认无界(实际是Integer.MAX_VALUE),但可传参设界;吞吐通常更高,但无界时可能 OOM -
SynchronousQueue:不存储元素,每个put必须配一个take;适合任务交接型场景(如线程池的DirectHandoff)
示例:用 ArrayBlockingQueue 实现基础模型
BlockingQueuequeue = new ArrayBlockingQueue<>(10); // 生产者 new Thread(() -> { for (int i = 0; i < 5; i++) { try { queue.put("item-" + i); // 阻塞直到有空位 System.out.println("produced: item-" + i); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }).start(); // 消费者 new Thread(() -> { for (int i = 0; i < 5; i++) { try { String item = queue.take(); // 阻塞直到有数据 System.out.println("consumed: " + item); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }).start();
如果必须用 synchronized + wait/notify,怎么写才不出错?
仅当需要自定义等待逻辑(比如超时、多条件组合)且无法用 BlockingQueue 替代时才考虑。核心守则只有三条:
- 必须在
synchronized块内调用wait()和notify() - 必须用
while循环检查条件,不能用if - 所有修改共享状态的操作,和所有
wait()的判断条件,必须使用同一把锁
错误写法:if (queue.isEmpty()) wait(); → 可能虚假唤醒后直接取空队列
正确写法:while (queue.isEmpty()) wait();
真正难的不是写出来,而是想清楚「谁负责唤醒」「唤醒是否及时」「中断是否被正确传播」——这些细节在 BlockingQueue 里已被反复验证过,自己重造轮子时最容易在日志里看到 IllegalMonitorStateException 或线程卡死,却查不出哪一行锁没对上。










