synchronized 的非公平性:通过监控 Monitor 的 EntryList 与 WaitSet 分析为什么唤醒线程不保证公平性
synchronized的非公平性源于锁释放后JVM从EntryList中随机挑选一个线程唤醒,不按先进先出顺序,因此新线程与等待已久的线程中签概率完全相同。此外,WaitSet中的线程被notify后需重新进入EntryList排队,等待再次获取锁,这进一步加剧了非公平性。
说说 synchronized 的非公平性这事儿。很多人容易把锅甩给 wait/notify,其实根子不在这——真正的关键,是锁释放之后,JVM 到底按什么规则从 EntryList 里挑线程去唤醒。结论先放在这里:它不按先来后到,而是随机选。换句话说,你排了半天队,可能还不如一个新来的“插队”快。
EntryList:一个没有“先来后到”概念的等候区
当一个线程试图进入 synchronized 块,但锁已经被别人占着,它不会去哪儿排个号,而是被塞进一个单向链表结构——也就是 EntryList。这个结构本身不记录线程进入的时间,也不具备任何排序能力。
几个关键点值得记住:
- 线程进了 EntryList,状态变成 BLOCKED,但这只表示“我在等锁”,不代表“我排在最前面”
- 锁释放的那一刻,JVM 从 EntryList 里随便挑一个线程唤醒。挑谁呢?由底层 C++ 实现说了算,跟等待时间长短没有任何关系
- 新来报到的线程,和已经在 EntryList 里蹲了很久的线程,中签概率一模一样
说白了,这就像一个没人维持秩序的候车厅,门一开谁挤得快谁先上。
WaitSet 里的线程,醒了也得重新排队
调用 wait() 的线程会主动退出临界区、释放锁,然后一头扎进 WaitSet。等到被 notify()
- 哪怕它在 WaitSet 里等了一万年,被叫醒后也只是获得了一次“参赛资格”,而不是直接拿锁
- 它和刚来的新线程、已经排队等了几毫秒的线程,在锁竞争的牌桌上地位完全平等
这就造成了一个双重的非确定性:一方面,WaitSet 里哪个线程先被唤醒本身就不确定;另一方面,被唤醒后挤进 EntryList,能不能赢下锁又是另一回事。两重不确定性叠加,公平性基本无从谈起。
JVM 压根没给公平模式留后门
跟 ReentrantLock(true) 显式开启的公平模式不同,synchronized 的底层 Monitor 实现——也就是 ObjectMonitor——在 JVM 源码里根本就没设计公平调度的路径。
- _EntryList 字段就是一个普通指针链表,不带时间戳,也没有计数器字段来记录谁先谁后
- 唤醒逻辑集中在
ObjectMonitor::exit()里,调用的是平台相关的线程调度接口,而不是从队列里依次取元素 - 设计上放弃公平性,核心意图是为了减少上下文切换的成本,同时避免所谓的“车队效应”——即一个慢线程拖慢整条等待链上的所有线程
所以这不是 bug,是设计选择,而且是一个很务实的选择。
公平性缺失不等于程序不安全
非公平不代表“错误”。只要程序满足了互斥、可见性和有序性这三个基本语义,逻辑上就是安全的。问题在于,你不能依赖线程的等待顺序来做任何时序上的假设。
- 如果你的业务逻辑依赖于严格的任务执行顺序——比如按提交队列依次处理——那别指望 synchronized 能帮你做到
- 需要公平语义的时候,直接用
ReentrantLock(true)加Condition组合拳 - 日常开发中,非公平反而往往是性能更优的选择:刚释放锁的线程大概率还在 CPU 缓存里,让它立即重入,比唤醒另一个远端的线程要快得多

总之,synchronized 的非公平性不是个“缺陷”,而是 JVM 工程师出于性能考量做的一个清醒取舍。理解了这个机制,你就能在并发编程中更清楚地判断:什么时候该用它,什么时候该换工具。


































