商城首页欢迎来到中国正版软件门户

您的位置: 首页 > 文章列表 > 编程开发 > 为什么 ReentrantLock 默认使用非公平锁?对比其在高并发竞争下的 CPU 调度吞吐差异。

为什么 ReentrantLock 默认使用非公平锁?对比其在高并发竞争下的 CPU 调度吞吐差异。

  发布于2026-07-10 阅读(0)

扫一扫,手机访问

先抛出结论:ReentrantLock 默认选择非公平锁,绝不是设计上的疏忽,而是在高并发场景下,用可接受的公平性代价换取了实实在在的吞吐提升——实测中通常能带来 20%–30% 的性能优势。至于“线程饥饿”的理论风险,在绝大多数实际业务中几乎不会触发。

那么,非公平锁在 lock() 这一步到底比公平锁快在哪?核心差异藏在第一步:是否绕过 AQS 队列直接抢锁。

NonfairSync 的 lock() 方法一上来就是 compareAndSetState(0, 1)——只要 state 为 0(锁空闲),新线程就能“插队”成功,完全跳过入队、唤醒、上下文切换这一整套开销。而 FairSync 的 lock() 则会优先调用 acquire(1),然后进入 tryAcquire(),里面第一件事就是检查当前线程是否排在队首:!hasQueuedPredecessors()。哪怕锁刚刚被释放,只要队列里还有等待者,新线程也得老老实实排队。

这一步看似微小,但在锁争抢激烈时会被急剧放大:

  • CPU 缓存行竞争更少:非公平锁减少了对 AQS 队列 head/tail 节点的频繁读写;
  • 上下文切换更少:避免了线程刚入队就被唤醒的“虚假唤醒”或“唤醒滞后”;
  • 指令流水线更友好:在 CAS 失败率较低时,分支预测成功率高,CPU 不易停顿。

公平锁的 hasQueuedPredecessors() 带来什么开销?

hasQueuedPredecessors() 看起来只是一次链表头尾判断,但它的代价远不止表面。因为它强制每次加锁前都要访问 AQS 队列的 headtail 节点——这两个字段都是 volatile 的,这意味着每次读取都会触发内存屏障,并可能引发缓存同步流量。在 10 万 TPS 的压测中,这个方法调用的开销能占到锁路径总耗时的 12%~18%,尤其是当 CPU 核心数多、NUMA 节点跨距大时,延迟抖动会明显上升。

更隐蔽的问题是:公平锁让“锁释放 → 下一个线程获取”之间的路径变长,中间夹着队列状态校验、节点插入、唤醒通知三步;而非公平锁在无竞争时只需单次 CAS 就能搞定。

实测吞吐差异在哪种场景最明显?

并非所有高并发场景都一样。非公平锁的优势在以下几种情况下尤为显著:

  • 临界区极短(如计数器自增、状态位翻转),线程持有锁的时间可以忽略不计;
  • 锁释放后立刻就有新请求涌入(例如事件循环、Netty 的 ChannelHandler);
  • CPU 核心数 ≥ 16,且线程数远超核心数,导致调度器频繁迁移线程。

反过来,如果临界区很长(如 IO 等待、复杂计算),或者业务明确需要按请求顺序处理(比如支付扣款必须严格 FIFO),那么公平锁的确定性反而更值得信赖。此时吞吐差距会缩小,甚至因避免了长尾延迟而让系统更稳定。

别忽略 unlock() 的隐含一致性成本

很多人只盯着 lock(),却忽略了 unlock() 同样受模式影响。非公平锁的 tryRelease() 只需修改 state 并清空 owner,而公平锁在唤醒下一个线程前,还得多做一次 isFirstInQueue() 判断(本质上是 hasQueuedPredecessors() 的逆操作),并确保唤醒的是真正的队首节点。

这个判断在高争用下可能失败重试——尤其是当多个线程同时释放锁、又同时触发唤醒逻辑时,JVM 层面无法原子化地完成“检查队首 + 唤醒”,只能靠循环 + CAS 来保证,这无疑进一步吃掉了 CPU 周期。

所以,ReentrantLock 默认选非公平,不是偷懒,而是把“多数时候更快”作为第一优先级。如果你确实需要公平,主动传 true 就行,但同时也要接受那部分可测量的吞吐折损和延迟抖动。

本文转载于:https://www.php.cn/faq/2393289.html 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

热门关注