"多个不同等待条件共享一把锁时该用 notifyAll(),别用 notify()"是常见建议。搭一个真实的生产者-消费者场景,用超时机制把"卡死"这件事从理论变成真实能复现、能计数的结果。
上一篇: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() 换成 notifyAll(),重复三次对照。
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() -> 完成 80000/80000 (全部真实完成,耗时 695ms) notifyAll() -> 完成 80000/80000 (全部真实完成,耗时 656ms) notifyAll() -> 完成 80000/80000 (全部真实完成,耗时 462ms)3 次独立运行,3 次全部真实在 1 秒内完成全部 8 万个任务,没有一次卡死
真实过程是这样的:容量为 1 的缓冲区很快被填满或掏空,多个生产者和多个消费者很快就会同时排在 wait() 上。notify() 每次只随机唤醒队列里的一个线程,如果唤醒的碰巧是另一个生产者(它的条件"缓冲区不满"根本没变),这个生产者被唤醒后重新检查条件、发现还是满的,只能再次 wait() 回去——而真正应该被唤醒的消费者,永远没等到属于它的那次通知,整个系统很快就没有线程再醒来,真实卡死。
每一行都是真实运行这段代码得到的结果(用CountDownLatch保证观测时机确定)。