acquire(int)与acquireShared(int)核心差异在尝试获取和唤醒传播:前者用tryAcquire返回布尔值,后者用tryAcquireShared返回≥0才成功且支持传播;Node.SHARED/EXCLUSIVE标记决定唤醒行为;ReadWriteLock将state拆为高低16位分别计数读写锁。

acquire(int) 与 acquireShared(int) 的调用路径差异
两者都走 AQS 模板方法,但入口函数名和后续分支完全不同:acquire(int) 触发独占流程(如 ReentrantLock.lock()),acquireShared(int) 触发共享流程(如 ReadLock.lock())。关键区别不在排队逻辑,而在「尝试获取」和「唤醒传播」两步。
独占模式下,tryAcquire(int) 返回 true 表示成功,失败则入队;共享模式下,tryAcquireShared(int) 必须返回 ≥0 才算成功,且该返回值会被用于后续传播判断——比如 CountDownLatch 在 state == 0 时返回 1,否则返回 -1。
常见错误现象:重写 tryAcquireShared 时直接 return true/false,编译能过但语义错乱,导致 doAcquireShared 死循环或无法唤醒后继。
Node.SHARED 与 Node.EXCLUSIVE 节点类型决定唤醒行为
AQS 队列里混排着两种节点:Node.EXCLUSIVE(值为 null)表示独占等待者,Node.SHARED(静态实例)表示共享等待者。这个标记不参与 CAS,但被 shouldParkAfterFailedAcquire 和 setHeadAndPropagate 显式读取。
释放锁时:release(int) 只调用 unparkSuccessor(head) 唤醒一个后继;而 releaseShared(int) 调用 doReleaseShared(),它会自旋 + CAS 尝试唤醒至少一个后继,且被唤醒的共享节点若发现资源仍充足(如 tryAcquireShared 再次返回 ≥0),会继续传播唤醒——这就是“批量唤醒”的根源。
容易踩的坑:ReentrantReadWriteLock 的读锁和写锁共用同一队列,但写锁释放后必须确保不误唤醒读线程(反之亦然),所以其 tryReleaseShared 里有严格的位运算校验,高 16 位和低 16 位不能交叉污染。
state 字段的语义拆分是 ReadWriteLock 的关键设计
ReentrantLock 把 state 当作整数计数器(0=空闲,>0=重入次数);ReentrantReadWriteLock 则把 int state 拆成两个 16 位段:(state >>> 16) 是读锁计数,state & 0xFFFF 是写锁计数。
这种拆分带来三个硬约束:
- 读锁最大并发数为 65535,超限抛
Error(不是Exception) - 写锁重入次数也受限于低 16 位,同样最多 65535 次
- 读锁释放时必须保证不干扰写锁状态位,所以
tryReleaseShared用的是无符号右移 + CAS 循环,不是简单减法
性能影响明显:每次读锁操作都要做两次位运算 + 一次 CAS,比纯独占锁多约 15%~20% 指令开销;但换来的是读多场景下的吞吐优势。
为什么 ReentrantReadWriteLock 不用两个独立 AQS 实例
有人会想:既然读写逻辑不同,为何不给读锁、写锁各配一个 AQS?答案是公平性与唤醒顺序无法保障。如果两个队列独立,写线程在读队列里插队就失去意义,也无法实现「写锁优先唤醒」或「读锁降级」等语义。
真实情况是:Sync 类只继承一个 AQS,所有线程(读/写)进同一 CLH 队列,靠节点类型(SHARED/EXCLUSIVE)和 state 位段共同决策是否能抢到资源。这也解释了为什么写锁获取时要检查「是否有读线程正在运行」——它得遍历队列或查 state 高位,而不是看另一个队列空不空。
最容易被忽略的一点:共享节点被唤醒后,不是直接执行业务代码,而是再次调用 tryAcquireShared 确认资源可用性。这意味着即使被唤醒,也可能因资源被新来的写线程抢占而再次挂起——这层重试逻辑常被误认为是 bug。

















