虚假唤醒是线程未被notify、中断或超时却自行从wait()返回,属jvm规范允许行为;必须用while而非if包裹wait()以重检条件,否则可能执行错误逻辑导致异常或数据不一致。

虚假唤醒到底是什么,为什么if会出事
虚假唤醒就是线程没被notify()、没中断、也没超时,却自己从wait()里醒了——这在 JVM 规范里是明确允许的,不是 bug,是设计。它可能来自操作系统信号抖动、调度器优化,甚至某些硬件中断行为。一旦你用if判断条件后就直接执行业务,醒来的线程会跳过条件重检,大概率读到错误状态(比如number == 0时还去pop()),直接抛异常或破坏数据一致性。
常见错误现象:
- 生产者消费者程序偶尔卡死、打印乱序、甚至
ArrayIndexOutOfBoundsException - 多线程轮转打印(A→B→C)突然顺序错乱,比如 C 打印完还没通知 A,A 却抢着执行了
- 用
notify()本想精准唤醒一个,结果多个线程都醒了,但只有第一个能干活,其余全在错误状态下继续往下跑
必须用while包裹wait()的实操逻辑
这不是“建议”,是强制要求。JDK 文档写得非常直白:“spurious wakeups are possible, and this method should always be used in a loop”。核心就一条:每次从wait()返回,都得重新检查条件是否真满足。
-
while (number == 0) { this.wait(); }—— 正确:醒来立刻再判,不满足就继续等 -
if (number == 0) { this.wait(); }—— 危险:醒来只判一次,之后不管真假都执行后续逻辑 - 哪怕你只启两个线程、逻辑极简,虚假唤醒概率低,也不能赌;高并发下它一定会在某个凌晨三点复现
notify() vs notifyAll() 和虚假唤醒的关系
很多人以为换用notify()就能避免虚假唤醒——其实完全无关。notify()只是减少唤醒数量,但只要被唤醒的线程条件不成立,它照样是虚假唤醒。而notifyAll()虽然唤醒所有人,反而让所有线程都进入while重检流程,客观上更安全(尤其在条件复杂、多个等待逻辑共存时)。
- 用
notify()的前提:你100%确定只有一个线程在等这个条件,且唤醒后它一定可以执行(比如严格一对一配对) - 用
notifyAll()更常见:比如库存变化,生产者和消费者都在等,谁该醒得由条件说了算,不是由唤醒动作决定 - 性能影响很小:现代 JVM 对空
wait()返回极快,真正耗时的是锁竞争,不是多判几次while
用ReentrantLock + Condition时同样要while
别以为换了 JUC 就能松口气。Condition.await()一样存在虚假唤醒,JDK 文档原话:“spurious wakeups are possible, and this method should always be used in a loop”。它和Object.wait()面对的是同一个底层问题。
- 错误写法:
if (count == 0) notEmpty.await(); - 正确写法:
while (count == 0) notEmpty.await(); -
Condition的优势在于能建多个等待队列(比如notFull/notEmpty),但它不解决“醒来要不要再确认”这个问题——那永远是程序员的责任
最常被忽略的一点:虚假唤醒不是“理论风险”,它是 JVM 的正式契约。你写的每个wait(),无论用什么锁、跑在什么系统上,都得默认它随时可能无理由醒来。不加while,等于把条件判断的控制权交给了调度器。










