Java监视器有等待队列,为什么synchronized还是非公平锁?

Java监视器有等待队列,为什么synchronized还是非公平锁?
XRJava监视器有等待队列,为什么synchronized还是非公平锁?
之前在看synchronized的源码时发现了一个有趣的问题:既然Java的监视器(Monitor)底层有等待队列(Entry Set)来管理竞争锁的线程,那为什么synchronized还是实现不了公平锁呢?
其实准确来说,不是”实现不了”,而是synchronized从设计之初就选择了性能和简便性,所以压根没打算做成公平锁。
监视器的基本结构
每个Java对象都关联着一个监视器,包含几个关键部分:
- Owner: 当前持有锁的线程
- Entry Set: 等待获取锁的线程队列
- Wait Set: 调用wait()后进入等待状态的线程队列
线程获取synchronized锁的流程:
- 锁没人用(Owner为空)→ 直接拿到锁
- 锁被占用 → 进入Entry Set排队等待
synchronized为什么是非公平的?
虽然有Entry Set这个队列,但synchronized的锁分配策略就是非公平的,主要原因:
1. 新线程可以”插队”
这是最关键的点。当锁释放时,新来的线程可以直接和Entry Set中等待的线程竞争,而不需要排队。
举个例子:
- 线程A释放锁
- 线程B在Entry Set中等待
- 线程C刚好这时候要获取锁
- 结果:线程C可能直接抢到锁,跳过了线程B
为什么要这样设计?性能考虑。让新线程直接竞争可以减少线程上下文切换的开销,提升整体吞吐量。
2. JVM的各种锁优化
现代JVM对synchronized做了很多优化,这些优化进一步加剧了非公平性:
偏向锁(Biased Locking)
- 锁会”偏心”第一个获取它的线程
- 后续如果没有竞争,直接进入同步代码,根本不走队列
轻量级锁(Lightweight Locking)
- 通过CAS操作尝试获取锁
- 失败后会自旋(spin)而不是直接入队
- 新线程可能通过自旋抢到锁
自旋锁(Spin Locking)
- Entry Set中的线程可能自旋尝试获取锁
- 和新线程竞争时没有顺序保证
3. 队列唤醒策略不保证FIFO
Entry Set中线程的唤醒顺序并不是先进先出的。JVM实现(比如HotSpot)可能采用:
- 随机唤醒
- 策略性唤醒(优先唤醒最近活跃的线程)
而不是按请求顺序分配锁。
公平锁是什么样的?
看看ReentrantLock的公平锁实现就明白了:
1 | ReentrantLock fairLock = new ReentrantLock(true); // true表示公平锁 |
公平锁的策略:
- 锁释放时,优先唤醒Entry Set中等待时间最长的线程
- 新线程必须排队,不能插队
synchronized的局限:
- 没有公平模式选项
- 无法控制Entry Set中线程的唤醒顺序
- 底层实现始终是非公平的
为什么synchronized选择非公平?
从技术角度看,Entry Set完全可以设计成FIFO队列,JVM也完全可以按FIFO顺序唤醒线程。synchronized实现公平锁在技术上没有障碍。
真正的原因是设计取舍:
性能优先的考虑
减少线程切换开销
1 | 场景:线程A释放锁后 |
提升吞吐量
- 非公平锁在高并发场景下允许更高效的锁竞争
- 减少CPU空闲时间,最大化利用率
JVM优化策略的配合
现代JVM对synchronized的各种优化(偏向锁、轻量级锁、自旋锁等)都更适合非公平锁的特性。如果强行实现公平锁,这些优化的效果会大打折扣。
总结
synchronized的非公平性不是技术限制,而是设计选择。Java在设计时优先考虑了性能和简便性,选择牺牲公平性来换取更好的吞吐量。
如果你的业务场景对公平性有严格要求,可以考虑使用ReentrantLock的公平模式。但要记住,公平锁通常会带来一定的性能损失。
什么时候需要公平锁?
- 防止线程饥饿的场景
- 对响应时间有严格要求的系统
- 需要严格按顺序处理请求的业务逻辑
什么时候用synchronized就够了?
- 大部分普通的同步场景
- 追求高吞吐量的系统
- 对公平性要求不高的业务逻辑











