在Java多线程编程中,线程间的协作是个绕不开的话题。wait()
、notify()
、notifyAll()
是最经典的同步工具,但你有没有想过,有时候线程被唤醒了,却发现压根儿没啥活儿干?这就叫"虚假唤醒"(Spurious Wakeup)。今天咱就从最朴素的方案聊起,一步步推演问题,最后逼近现代最靠谱的解决办法,顺便看看每一步能咋优化。
最朴素的方案:直接用 wait() 和 notify()
假设我们写个简单的生产者-消费者模型:生产者往队列里塞数据,消费者取数据。队列空了,消费者就等着;队列满了,生产者也等着。代码大概长这样:
java
class Queue {
private int item;
private boolean hasItem = false;
public synchronized void produce(int value) {
while (hasItem) {
try {
wait(); // 队列有东西就等着
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
item = value;
hasItem = true;
notify(); // 通知消费者
}
public synchronized int consume() {
while (!hasItem) {
try {
wait(); // 队列没东西就等着
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
hasItem = false;
notify(); // 通知生产者
return item;
}
}
这代码看起来挺直白吧?生产者塞了个值就唤醒消费者,消费者取完值再唤醒生产者。但问题来了:wait()
有可能被"虚假唤醒"打断。啥意思呢?就是线程莫名其妙醒了,但条件(比如 hasItem
)压根没变。这不是代码逻辑问题,而是操作系统或者 JVM 实现上的小概率事件。
不利因子 :虚假唤醒导致线程醒来后直接跑下去,可能操作了不符合预期的数据。比如消费者醒来发现队列还是空的,却因为没检查条件直接访问 item
,结果要么数据错乱,要么抛异常。
优化方向 :得有个机制确保醒来后条件真满足了再继续,不能光靠 wait()
被唤醒就傻乎乎往下跑。
第一步改进:加个条件检查
虚假唤醒的本质是"醒了不代表条件ready"。那咋办?简单,加个循环检查呗!把 wait()
包在 while
里,确保条件不满足就继续等。这也是 Java 官方文档推荐的做法:
java
public synchronized void produce(int value) {
while (hasItem) { // 条件不满足就一直等
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
item = value;
hasItem = true;
notify();
}
public synchronized int consume() {
while (!hasItem) { // 条件不满足就一直等
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
hasItem = false;
notify();
return item;
}
这招其实已经解决了虚假唤醒的核心问题。while
循环保证了线程醒来后会再瞅一眼条件,比如消费者发现 !hasItem
还是 true,就老老实实回去接着睡。
数字敏感小验证 :假设队列容量是 1,生产者塞了 3 次,消费者取了 2 次。如果没虚假唤醒,队列里应该剩 1 个元素。但如果有虚假唤醒,消费者可能多取一次,hasItem
变 false,生产者再塞就覆盖数据,逻辑就乱了。加了 while
后,虚假唤醒顶多让线程多醒一次,但不会出错。
不利因子 :虽然逻辑安全了,但 notify()
太粗暴。它只唤醒一个线程,万一唤醒的是另一个等着塞数据的生产者,而不是等着取数据的消费者咋办?线程就白醒了,效率不高。
优化方向:能不能精准唤醒"对的那个人"?或者干脆用更高级的工具,少点这种随机性。
再进一步:换成 notifyAll()
既然 notify()
不靠谱,那就用 notifyAll()
,把所有线程都叫醒,总有一个是对的吧:
java
public synchronized void produce(int value) {
while (hasItem) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
item = value;
hasItem = true;
notifyAll(); // 全部唤醒
}
public synchronized int consume() {
while (!hasItem) {
try {
wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
hasItem = false;
notifyAll(); // 全部唤醒
return item;
}
这下虚假唤醒更不是事儿了,反正醒来的线程都会检查条件,不行的继续睡,行的干活儿。逻辑上没毛病。
不利因子 :但你想啊,线程多了怎么办?假设有 10 个生产者、10 个消费者,每次 notifyAll()
把 20 个线程全叫醒,结果只有 1 个能干活,剩下 19 个白忙活,CPU 不得忙疯?这效率也太感人了。
优化方向:能不能精细化管理线程,别老是"全家出动"?现代方案肯定得有点"点对点"的味道。
现代王牌:用 Lock 和 Condition
到了今天,Java 提供的 java.util.concurrent.locks
包才是主流。咱们用 ReentrantLock
和 Condition
重写一下:
java
import java.util.concurrent.locks.*;
class Queue {
private int item;
private boolean hasItem = false;
private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();
private final Condition notEmpty = lock.newCondition();
public void produce(int value) {
lock.lock();
try {
while (hasItem) {
notFull.await(); // 等队列不满
}
item = value;
hasItem = true;
notEmpty.signal(); // 唤醒等着取的线程
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
lock.unlock();
}
}
public int consume() {
lock.lock();
try {
while (!hasItem) {
notEmpty.await(); // 等队列不空
}
hasItem = false;
notFull.signal(); // 唤醒等着塞的线程
return item;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return -1; // 假设出错返回-1
} finally {
lock.unlock();
}
}
}
这方案牛在哪?
- 精准唤醒 :
Condition
分了notFull
和notEmpty
,生产者只唤醒消费者,消费者只唤醒生产者,不像notifyAll()
瞎喊一通。 - 虚假唤醒免疫 :
while
循环还在,醒来后条件不对就接着等。 - 灵活性拉满 :
Lock
比synchronized
更细粒度,还能支持公平锁、读写锁啥的。
数字验证:假设 5 个生产者、5 个消费者,队列容量 1。每次生产或消费,最多只唤醒 1 个"对的"线程,而不是 9 个"无关的"。效率提升不是一点半点。
总结:从朴素到复杂的进化
- 朴素起点 :
wait()
+notify()
,简单但碰上虚假唤醒就懵。 - 初级优化 :加
while
检查条件,逻辑安全但唤醒不精准。 - 中级改进 :
notifyAll()
全唤醒,稳是稳了但效率低。 - 现代方案 :
Lock
+Condition
,精准、高效、抗虚假唤醒,完美贴合并发需求。