NonfairSync.lock()先CAS抢锁是为提升性能,减少线程挂起开销;FairSync则通过hasQueuedPredecessors()强制排队,确保公平性但增加volatile读开销。

NonfairSync.lock() 为什么总先抢一次
非公平锁的 lock() 方法在进入等待队列前,会用 CAS 直接尝试修改 state 值为 1。成功就直接占有锁、设置 exclusiveOwnerThread;失败才走 acquire(1) 进入 AQS 标准流程。这意味着:哪怕队列里已有线程在等,新来的线程仍可能“插队”成功。
这种设计不是 bug,是明确的性能取舍——减少线程挂起/唤醒开销,尤其在锁持有时间短、竞争不激烈的场景下,吞吐量明显更高。
- 源码路径:
NonfairSync.lock()→compareAndSetState(0, 1) - 典型现象:高并发下,
tryLock()成功率比公平锁高,但个别线程可能连续几次都抢不到,出现短暂“饥饿” - 注意:
acquire(1)内部调用的tryAcquire仍是非公平逻辑,不是“进队后再公平排队”
FairSync.tryAcquire() 如何强制排队
公平锁的 tryAcquire 在判断 state == 0 后,**多做了一步检查**:!hasQueuedPredecessors()。这个方法本质是判断当前线程是否排在同步队列 head 后第一个有效节点(即是否真正在队首),只有为 true 才允许获取锁。
换句话说:即使锁空闲,只要队列非空(哪怕只有一个线程在等),新线程也必须老老实实加到队尾,不能跳过前面的人。
- 关键代码:
if (c == 0 && !hasQueuedPredecessors() && compareAndSetState(0, acquires)) - 代价:每次
lock()都要读一次队列头尾指针,且hasQueuedPredecessors()是 volatile 读,比纯 CAS 开销略大 - 副作用:在锁争抢剧烈时,
acquire调用频次上升,AQS 队列节点创建和 CAS 自旋次数增加
waitStatus 和 unparkSuccessor 怎么体现调度差异
AQS 的 FIFO 队列本身对公平/非公平不做区分,真正起作用的是「谁被 unpark」以及「何时 unpark」。
unparkSuccessor(Node node) 总是从 node.next 开始向后找第一个 waitStatus 的节点唤醒。但在公平锁下,由于新线程从不插队,head 后第一个节点大概率就是等待最久的那个;而非公平锁下,刚唤醒的线程可能立刻又抢到锁,而队列中间的节点还在沉睡。
- 调试线索:打断点在
unparkSuccessor,观察每次唤醒的Node.thread是否符合预期顺序 - 注意:
waitStatus = CANCELLED(值为 1)的节点会被跳过,所以队列中实际有效节点数 ≠queue.size() - 非公平锁的「唤醒即抢锁」行为,使得
shouldParkAfterFailedAcquire中的 park 次数可能远少于公平锁
构造参数 false/true 对 sync 字段的直接影响
ReentrantLock 的构造函数只决定 sync 字段初始化为 FairSync 还是 NonfairSync 实例,后续所有加锁、释放、条件变量操作,都通过该实例的重写方法分发。
没有中间层切换逻辑,也没有运行时动态变更机制。一旦实例化,公平性就固定了。
- 默认构造:
new ReentrantLock()→sync = new NonfairSync() - 显式指定:
new ReentrantLock(true)→sync = new FairSync();new ReentrantLock(false)仍是NonfairSync - 容易忽略的点:公平锁的「公平」仅作用于「尚未获得锁的线程」;已持有锁的线程重入时,不参与排队,直接 state++
公平锁的队列调度逻辑藏在 hasQueuedPredecessors() 里,而不是队列结构本身;非公平锁的“不公平”也不是完全随机,而是由「抢锁优先级高于排队」这一条规则驱动。真正影响性能的,往往不是锁本身,而是你是否让锁的调度策略匹配了业务线程的实际等待模式。










