本文深入剖析多消费者场景下因错误使用 wait/notify 导致的虚假唤醒与竞争条件,指出核心缺陷在于条件检查与操作未在同一个同步块内完成,并提供符合规范的线程安全实现方案。
本文深入剖析多消费者场景下因错误使用 wait/notify 导致的虚假唤醒与竞争条件,指出核心缺陷在于条件检查与操作未在同一个同步块内完成,并提供符合规范的线程安全实现方案。
在 Java 中实现生产者-消费者模式时,若未严格遵循 wait()/notifyAll() 的经典使用范式,极易引发隐蔽死锁或无限等待——程序看似运行,却无任何输出(如题中 Consumer1/Consumer2 完全静默),根本原因并非线程阻塞于锁争用,而是陷入虚假唤醒(spurious wakeup)+ 条件失效(lost condition) 的组合陷阱。
关键问题在于:你的 Consumer1Thread.run() 中,waitOnEmpty() 与后续 peek()/poll() 操作分属不同同步上下文:
// ❌ 错误示范:条件检查与操作分离
while (isEmpty()) {
waitOnEmpty(); // 在 EMPTY_LOCK 上 wait → 释放锁并挂起
}
// 此处已退出 synchronized(EMPTY_LOCK),其他线程可修改队列!
Integer num = peek(); // 可能再次为空!
if (num != null && num % 2 == 0) {
int n = poll(); // 若此时队列为空,poll() 返回 null 或抛异常
notifyAllForFull();
System.out.println("Consumer 1 (even): " + n);
}当两个消费者同时等待 EMPTY_LOCK,生产者插入一个数后调用 notifyAllForEmpty(),两个消费者均被唤醒。但只有一个能成功 poll(),另一个在 peek() 后发现队列已空,却因未再次检查条件而直接执行 poll()(可能返回 null)或陷入无限循环——更糟的是,若 peek() 返回 null 而代码未处理,num % 2 将抛 NullPointerException,线程意外终止。
✅ 正确做法:将条件判断、等待、操作全部置于同一把锁的临界区内,确保原子性:
立即学习“Java免费学习笔记(深入)”;
你接手一个需求时,是不是经常这样:"改一个小字段 → 结果把不相关的代码也改了"、"需求说得很模糊 → 做着做着发现跑偏了"、"数据库加个字段 → 上线才发现漏了数据兼容"、"做了个新功能 → 一个月后自己都忘了当时怎么设计的"、"写到一半被中断 → 再回来不知道从哪继续";这个 Skill 就是专门解决这些问题的。 它会自动探测你的项目环境(JDK、Spring Boot、ORM、数据库、前端框架),记在
class NumbersThread extends Thread {
protected final Queue<Integer> numbersQueue;
private final Object queueLock = new Object(); // 统一使用单锁,简化逻辑
private final int capacity = 64;
// ... 构造器等
protected boolean isEmpty() {
synchronized (queueLock) {
return numbersQueue.isEmpty();
}
}
protected boolean isFull() {
synchronized (queueLock) {
return numbersQueue.size() >= capacity;
}
}
protected Integer peek() {
synchronized (queueLock) {
return numbersQueue.peek();
}
}
protected Integer poll() {
synchronized (queueLock) {
return numbersQueue.poll();
}
}
protected void add(Integer n) {
synchronized (queueLock) {
numbersQueue.add(n);
}
}
// 等待/通知方法需在 queueLock 上操作(移除 FULL_LOCK/EMPTY_LOCK 两把锁)
protected void waitForNotFull() throws InterruptedException {
synchronized (queueLock) {
while (isFull()) {
queueLock.wait(); // 等待队列不满
}
}
}
protected void waitForNotEmpty() throws InterruptedException {
synchronized (queueLock) {
while (isEmpty()) {
queueLock.wait(); // 等待队列非空
}
}
}
protected void signalAll() {
synchronized (queueLock) {
queueLock.notifyAll(); // 通知所有等待者
}
}
}ProducerThread 改写(关键:notifyAll 在同一锁内):
class ProducerThread extends NumbersThread {
ProducerThread(Queue<Integer> numbersQueue) {
super(numbersQueue);
}
@Override
public void run() {
Random rand = new Random();
try {
while (!Thread.currentThread().isInterrupted()) {
synchronized (queueLock) {
while (isFull()) {
queueLock.wait();
}
int num = rand.nextInt(50);
numbersQueue.add(num);
System.out.println("Produced: " + num);
queueLock.notifyAll(); // ✅ 唤醒所有等待者(消费者/其他生产者)
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}Consumer1Thread 改写(关键:peek/poll 与条件检查同锁):
class Consumer1Thread extends NumbersThread {
Consumer1Thread(Queue<Integer> numbersQueue) {
super(numbersQueue);
}
@Override
public void run() {
try {
while (!Thread.currentThread().isInterrupted()) {
synchronized (queueLock) {
while (isEmpty()) {
queueLock.wait();
}
// ✅ 此时仍在 queueLock 同步块内,队列必不为空
Integer num = numbersQueue.peek();
if (num != null && num % 2 == 0) {
int n = numbersQueue.poll(); // 安全取出
System.out.println("Consumer 1 (even): " + n);
queueLock.notifyAll(); // 通知生产者或其他消费者
}
// 若不满足条件(如为奇数),不 poll,继续循环检查
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}Consumer2Thread 同理,仅修改条件为 num % 2 != 0。
⚠️ 重要注意事项:
- 弃用双锁(FULL_LOCK/EMPTY_LOCK):多把锁易引发死锁且无法保证条件一致性。单锁 queueLock 是最佳实践。
- 永远用 while 而非 if 检查条件:防止虚假唤醒后条件已不成立。
- notifyAll() 而非 notify():因存在多个语义不同的消费者(偶/奇),需唤醒所有等待者由其自行判断。
- 显式处理中断:捕获 InterruptedException 后恢复中断状态,确保线程可被优雅终止。
- peek() 后必须 poll():避免重复消费,且二者必须在同一锁内完成。
通过以上重构,生产者与消费者严格遵循「检查-等待-操作-通知」原子闭环,彻底规避了因条件失效导致的静默死锁,确保偶数/奇数被各自消费者稳定、无遗漏地处理。

















