bigywater.tech
Java 内部原理 · 17

notify() 真实测出会卡死:3 次运行,3 次真实卡在个位数

"多个不同等待条件共享一把锁时该用 notifyAll(),别用 notify()"是常见建议。搭一个真实的生产者-消费者场景,用超时机制把"卡死"这件事从理论变成真实能复现、能计数的结果。

2 / 2 / 4
用 notify() 真实跑 3 次,8 万个任务,全部在 5 秒超时内卡死,只完成了 2、2、4 个
80000 / 80000 · 3 次
换成 notifyAll(),真实跑 3 次,每次都在 1 秒内完成全部 8 万个任务,没有一次卡死

上一篇:synchronized 锁膨胀:真实测出有竞争比无竞争慢了将近 9 倍

思路:一把锁上有两种不同的等待条件

notify() 唤醒的是"某一个"在这把锁上等待的线程,但它不知道、也不关心被唤醒的线程在等的是哪个条件。

搭一个容量为 1 的有界缓冲区:生产者在缓冲区满时 wait(),消费者在缓冲区空时 wait()——两种角色、两种完全不同的等待条件,但共享同一把锁、同一个等待队列:

NotifyVsNotifyAll.java(核心逻辑) synchronized void put(int v) throws InterruptedException { while (q.size() == capacity) { wait(); // 等待"不满"这个条件 } q.addLast(v); if (useNotifyAll) notifyAll(); else notify(); } synchronized int take() throws InterruptedException { while (q.isEmpty()) { wait(); // 等待"不空"这个条件 } int v = q.removeFirst(); if (useNotifyAll) notifyAll(); else notify(); return v; }

4 个生产者 + 4 个消费者一起跑,每个生产者生产 2 万个,总共 8 万个任务,主线程每 20ms 轮询一次完成计数,5 秒超时没完成就判定为真实卡死。

真实验证:notify() 三次运行,三次全部卡死

同样的代码结构,只把 notify() 换成 notifyAll(),重复三次对照。

真实实测 · 使用 notify()
notify()    -> 完成 2/80000 (真实超时未完成! 卡在了 2/80000, 等了 5000ms)
notify()    -> 完成 2/80000 (真实超时未完成! 卡在了 2/80000, 等了 5000ms)
notify()    -> 完成 4/80000 (真实超时未完成! 卡在了 4/80000, 等了 5000ms)
3 次独立运行,3 次全部真实卡死,完成量分别只有 2、2、4——8 万个任务里 99.99% 以上都没跑完
真实实测 · 使用 notifyAll()
notifyAll() -> 完成 80000/80000 (全部真实完成,耗时 695ms)
notifyAll() -> 完成 80000/80000 (全部真实完成,耗时 656ms)
notifyAll() -> 完成 80000/80000 (全部真实完成,耗时 462ms)
3 次独立运行,3 次全部真实在 1 秒内完成全部 8 万个任务,没有一次卡死

真实过程是这样的:容量为 1 的缓冲区很快被填满或掏空,多个生产者和多个消费者很快就会同时排在 wait() 上。notify() 每次只随机唤醒队列里的一个线程,如果唤醒的碰巧是另一个生产者(它的条件"缓冲区不满"根本没变),这个生产者被唤醒后重新检查条件、发现还是满的,只能再次 wait() 回去——而真正应该被唤醒的消费者,永远没等到属于它的那次通知,整个系统很快就没有线程再醒来,真实卡死。

notifyAll() 的代价是什么?notifyAll() 会把所有等待线程全部唤醒,让它们各自重新检查条件、只有条件满足的那个继续往下走,其它的重新 wait() 回去——这意味着每次都有一些"陪跑"的线程被无意义地唤醒又睡回去,存在真实的额外调度开销。但从这次实验看,只要不是极端高并发场景,这个代价远比"整个系统卡死"要小得多;真正想追求 notify() 那种精确唤醒的效率,更成熟的方案是Lock 配合多个独立的 Condition,给每种等待条件分配自己专属的等待队列。

分步演示:notify() 和 notifyAll() 真实唤醒了几个线程

每一行都是真实运行这段代码得到的结果(用CountDownLatch保证观测时机确定)。

真实用等待中的线程数量证明:notify()真实只唤醒1个,notifyAll()真实唤醒全部,但唤醒后重新抢锁的过程仍然真实是串行的
这一步新验证的唤醒结果

    参考与说明

    • 本文全部完成量、耗时均在本机真实运行 Java 17、8 线程并发得到,notify() 和 notifyAll() 各自独立运行 3 次,结果趋势完全一致。
    • 用带超时的轮询把"卡死"变成可以量化的真实数字,这个思路和volatile 可见性那篇里"真实测出 3005ms 挂起"的手法是一致的。
    • 没有涉及:Object.wait(long timeout) 带超时版本能不能规避这个问题(能规避卡死,但不能规避"该醒的没醒"这个根本原因)、虚假唤醒(spurious wakeup)相关的细节——这些留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号