Spurious Wakeup(虚假唤醒)
在多线程编程中,Spurious Notification 或 Spurious Wakeup 是指一个线程在没有被显式信号(signal 或 notify)触发,或者所等待的条件尚未满足的情况下,自发地从等待状态(wait 或 await)中苏醒过来的现象。
这就是为什么 Okio 在注释中强调:必须在循环(loop)中使用 awaitSignal()。
1. 为什么会发生“虚假唤醒”?
这通常不是由你的程序代码错误引起的,而是由于底层操作系统、硬件架构或 JVM 的实现机制导致的:
- POSIX 标准允许: 在 Linux 等系统上,由于底层
pthread_cond_wait的实现原因,为了保证性能,系统可能会在没有信号时偶尔唤醒线程。 - 多处理器竞争: 在信号发出的瞬间,如果多个线程被唤醒,可能其中一个线程已经抢先修改了状态,导致后续唤醒的线程发现条件其实又不满足了。
2. 举例说明
假设我们有一个简单的队列,线程 A 正在等待队列中有数据(queue.notEmpty())。
错误写法(使用 if):
// ❌ 错误写法
if (queue.isEmpty()) {
timeout.awaitSignal(condition); // 如果发生虚假唤醒,线程会直接往下执行
}
takeData(); // 此时队列可能依然是空的,程序崩溃!
正确写法(使用 while 循环):
// ✅ 正确写法
while (queue.isEmpty()) {
timeout.awaitSignal(condition);
// 如果是虚假唤醒,循环会再次检查 isEmpty()
// 发现还是空的,就继续回去等待,从而“消除”了虚假通知的影响
}
takeData(); // 只有条件真的满足了,才会跳出循环
3. Okio 注释的深层含义
Okio 的 Timeout 类通过这种注释提醒开发者,awaitSignal() 的行为与 Java 原生的 Object.wait() 或 Condition.await() 是一致的。
核心要点总结:
- 持有锁: 调用
awaitSignal前必须先获得对应的锁(monitor)。 - 防御性循环: 永远不要假设线程醒来是因为条件满足了。必须使用
while (conditionIsNotMet)来包裹等待逻辑。 - 异常处理: 在 Okio 中,如果等待超时或者线程被中断,它会抛出
InterruptedIOException,这需要你进行捕获或向上抛出。
简单来说,“虚假通知”就像是闹钟偶尔会在没定点的时候自己响一下,你醒来后得看一眼表,发现还没到点,就得接着睡。