跳转到主内容
新派编程网 - 新潮编程理念,全新技术课堂!

怎么通过分析 AQS 的 Node 状态位竞争理解公平锁与非公平锁的吞吐量差异

Node.waitStatus直接影响线程是否立即抢锁:非公平锁跳过队列检查直接CAS抢占,公平锁需通过hasQueuedPredecessors()验证waitStatus有效性后才决定入队或放弃。 Node.waitStatus 怎么影响线程是否立即抢锁 公平锁和非公平锁在
tryAcquire
中的关键分歧,就藏在
hasQueuedPredecessors()
这个判断里——它本质是检查 AQS 同步队列头节点的后继是否为当前线程,也就是「队列里有没有人排在我前面」。这个检查依赖于 CLH 队列中每个
Node
的
waitStatus
字段(如
0
、
SIGNAL
、
CANCELLED
)是否被正确设置。 非公平锁跳过这步:只要
state == 0
,就直接
compareAndSetState(0, 1)
尝试抢占,哪怕队列里已有 5 个
Node
处于
SIGNAL
状态等待唤醒;而公平锁必须先调用
hasQueuedPredecessors()
,该方法会读取头节点的
next
字段并判断其
waitStatus
是否有效——若队列不为空,就放弃 CAS,乖乖入队。 这意味着:非公平锁的线程在锁释放瞬间有两次“免排队”机会(
lock()
初始 CAS +
tryAcquire
再次 CAS),而公平锁只有一次且受队列状态约束。 为什么非公平锁能减少 Node 入队频次 频繁入队意味着更多内存分配、CAS 更新
tail
指针、以及后续可能的线程挂起(
LockSupport.park()
)。这些操作都有开销,尤其在高竞争下会放大。 非公平锁下,一个刚释放锁的线程,很可能被下一个恰好执行
lock()
的线程直接抢到,根本不会走到
addWaiter()
和
acquireQueued()
公平锁强制所有新请求线程都走完整入队流程:新建
Node
→ CAS 设置为 tail → 设置前驱
waitStatus = SIGNAL
→ 才进入 park 即使队列里只有 1 个等待者,公平锁也拒绝“插队”,导致本可复用的 CPU 时间片被浪费在上下文切换上 实测场景(100 线程争抢单许可锁)中,非公平锁平均入队率约 30%,公平锁接近 95%——差异直接反映在
Node
实例数量和 GC 压力上。 公平锁的队列唤醒路径更长 公平锁要求「头节点释放后,只唤醒 successor」,这依赖
Node
的
waitStatus
被准确维护。但唤醒不是原子动作:
unparkSuccessor()
需要遍历
next
链表跳过
CANCELLED
节点,再 CAS 修改目标
Node
的
waitStatus
为
0
,最后调用
LockSupport.unpark()
。 X-Node企业快速建站1.0.6.0801 特色介绍: 1、ASP+XML+XSLT开发,代码、界面、样式全分离,可快速开发 2、支持语言包,支持多模板,ASP文件中无任何HTML or 中文 3、无限级分类,无限级菜单,自由排序 4、自定义版头(用于不规则页面) 5、自动查找无用的上传文件与空目录,并有回收站,可删除、还原、永久删除 6、增强的Cache管理,可单独管理单个Cache 7、以内存和XML做为Cache,兼顾性能与消耗 8、 下载 而非公平锁在
release()
后,如果发现
state
又被新线程通过初始 CAS 抢走,就完全绕过队列唤醒逻辑——连读
head.next
都省了。 关键点:
Node
的
waitStatus
不是单纯标记“等待中”,而是参与调度决策的状态机 公平性越强,对
Node
链表结构和
waitStatus
一致性的依赖越重 一旦发生虚假唤醒或
CANCELLED
节点堆积(比如超时 acquire 失败),公平锁的唤醒延迟会明显升高 实际选型时最容易忽略的细节 很多人以为「设成公平锁就能避免饥饿」,但现实是:如果业务本身存在长耗时临界区(比如锁内做 RPC 调用),哪怕用公平锁,后到的线程依然会在队列里等几十毫秒——此时吞吐量下降和响应毛刺并存,反而不如非公平锁配合合理超时控制来得稳定。 真正需要公平锁的场景极少,典型如:资源配额系统(必须严格按申请顺序扣减)、测试环境复现竞态、或监管合规要求「先到先服务」。其余绝大多数 Web/微服务场景,默认非公平锁 +
tryLock(timeout)
是更务实的选择。

相关文章