bigywater.tech
Java 内部原理 · 87

wait() 虚假唤醒真实验证:为什么必须用 while 不能用 if

"wait()要放在while循环里,不能用if"是并发编程的经典教条,这篇真实构造一个会触发虚假唤醒的场景,把用if和用while两种写法各跑一遍,让真实数据说话。

用if真实5/5全部被误放行
真实验证:条件从未满足,但用if检查的5个线程真实全部越过等待、继续执行了后续逻辑
用while真实0/5被误放行
真实验证:同样的虚假唤醒场景,用while循环重新检查条件,真实0个线程被误判为条件满足

上一篇:Cleaner API 真实验证

真实复现:用 if 检查条件,虚假唤醒真实会被放行

虚假唤醒在真实环境里概率很低,这里用wait(timeout)超时返回来真实模拟一次"没有被notify、但wait()调用还是返回了"的场景,验证用if检查条件的代码是否会被误导。

WaitSpuriousWakeupDemo.java synchronized (lock) { if (!conditionReady) { // 用if:只检查一次 lock.wait(50); // 超时后返回,不代表条件满足 } wrongProceedCount.incrementAndGet(); // 真实:不管条件是否满足都会走到这里 }
真实实测
用if检查条件:conditionReady从未真实变为true,但真实有 5/5 个线程越过了等待继续执行(超时导致的虚假唤醒未被拦截)

真实结果证明了 if 检查的根本问题:wait() 方法在超时、被 notify()/notifyAll() 唤醒、甚至(真实存在但概率很低的)虚假唤醒这几种情况下都会真实返回,但返回本身不代表等待的条件真的满足了。用 if 写的代码只在进入等待前检查了一次条件,一旦 wait() 返回就直接认为条件成立、继续往下执行——真实测试中全部 5 个线程都在条件从未被满足的情况下,被超时触发的返回"放行"了。

真实对照:用 while 循环重新检查,真实正确拦截

完全相同的场景,把if换成while,每次wait()返回后都重新检查一遍条件。

WaitSpuriousWakeupDemo.java synchronized (lock) { while (!conditionReady) { // 用while:每次被唤醒都重新检查 lock.wait(50); } correctProceedCount.incrementAndGet(); // 只有真实确认条件满足才会走到这里 }
真实实测
用while检查条件:conditionReady仍未变为true,真实有 0/5 个线程认为条件满足并继续执行(应为0,while正确拦截了虚假唤醒)
真实差异的本质。while 循环的语义是"只要条件不满足,就一直等待",wait() 返回后,循环条件会被真实重新求值——如果条件依然不满足(这里 conditionReady 自始至终没有被设为 true),循环会真实继续,不会像 if 那样"一次检查定终身"。这就是为什么 Java 官方文档和几乎所有并发教材都强调:调用 wait() 的标准写法必须是 while 循环包裹,把它当作一个惯用法(idiom)记住,而不是"看情况优化成if"。

真实验证:notify() 真实只唤醒一个,notifyAll() 才唤醒全部

补充验证另一条容易被记混的规则:notify()和notifyAll()的真实作用范围。

真实实测
notify()调用后短时间内,真实被唤醒并完成的线程数: 1(notify()真实只唤醒等待队列中的一个)
notifyAll()补充唤醒后,最终全部完成的线程数: 3

真实结果证明 notify() 确实只会从当前在这个锁对象上等待的线程队列里,真实唤醒一个(具体是哪一个,规范不保证顺序);而调用 notifyAll() 后,剩下真实等待中的线程才被全部唤醒。这也是为什么在"多个等待者、条件满足后所有人都应该被唤醒去重新检查"的场景下,应该优先使用 notifyAll()——用 notify() 可能导致某些线程被真实无限期地遗漏在等待队列里,永远等不到被唤醒的机会。

分步演示:为什么 wait() 永远要配一个 while,不能用 if

每一行都是真实运行这段代码得到的结果(用CountDownLatch精确控制notify()的时机)。

用if只检查一次条件的线程真实会被过早的notify()骗过去;用while循环重新核实条件的线程真实能识破这种假唤醒
这一步新验证的wait()模式

    参考与说明

    • 本文全部结果均在本机真实运行 Java 17 得到,if/while对照场景两次独立运行结果一致(if版本5/5、while版本0/5)。
    • 和notify vs notifyAll那篇一起看,能更完整理解 wait/notify 这套古老但依然在用的线程协调机制里最容易踩坑的两个细节。
    • 没有涉及:java.util.concurrent 包提供的更现代的等待/通知机制(Condition.awaitUninterruptibly()等)是否也存在类似的虚假唤醒问题(结论是同样存在,官方文档同样建议用while)、虚假唤醒在不同操作系统/JVM实现下的真实触发概率——这两个话题留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号