"wait()要放在while循环里,不能用if"是并发编程的经典教条,这篇真实构造一个会触发虚假唤醒的场景,把用if和用while两种写法各跑一遍,让真实数据说话。
上一篇:Cleaner API 真实验证
虚假唤醒在真实环境里概率很低,这里用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 个线程都在条件从未被满足的情况下,被超时触发的返回"放行"了。
完全相同的场景,把if换成while,每次wait()返回后都重新检查一遍条件。
WaitSpuriousWakeupDemo.java synchronized (lock) { while (!conditionReady) { // 用while:每次被唤醒都重新检查 lock.wait(50); } correctProceedCount.incrementAndGet(); // 只有真实确认条件满足才会走到这里 }
用while检查条件:conditionReady仍未变为true,真实有 0/5 个线程认为条件满足并继续执行(应为0,while正确拦截了虚假唤醒)
补充验证另一条容易被记混的规则:notify()和notifyAll()的真实作用范围。
notify()调用后短时间内,真实被唤醒并完成的线程数: 1(notify()真实只唤醒等待队列中的一个) notifyAll()补充唤醒后,最终全部完成的线程数: 3
真实结果证明 notify() 确实只会从当前在这个锁对象上等待的线程队列里,真实唤醒一个(具体是哪一个,规范不保证顺序);而调用 notifyAll() 后,剩下真实等待中的线程才被全部唤醒。这也是为什么在"多个等待者、条件满足后所有人都应该被唤醒去重新检查"的场景下,应该优先使用 notifyAll()——用 notify() 可能导致某些线程被真实无限期地遗漏在等待队列里,永远等不到被唤醒的机会。
每一行都是真实运行这段代码得到的结果(用CountDownLatch精确控制notify()的时机)。