核心区别在“谁能进”和“叫谁醒”:独占模式用tryAcquire返回布尔值决定准入,共享模式用tryAcquireShared返回负/0/正值控制是否传播唤醒;释放时独占只唤醒一个后继,共享则循环传播唤醒;state语义分别为重入计数与资源数量,ReadWriteLock更将其拆为高低16位。

核心区别不在排队机制,而在“谁能进”和“叫谁醒”这两件事的规则不同。
锁的准入逻辑不同
独占模式用 tryAcquire 判断是否能拿锁,返回 true/false: - true 表示成功抢到,线程继续执行 - false 表示失败,线程被封装成 EXCLUSIVE 节点进队列等待
共享模式用 tryAcquireShared 判断,必须返回 int 值: - 负数:失败,进队 - 0:成功,但不唤醒后续节点 - 正数:成功,且要触发唤醒传播(比如还有剩余许可)
这个返回值差异直接决定后续是否“连带叫醒别人”,不是可有可无的细节,而是共享语义的根基。
释放后的唤醒行为不同
独占锁释放后调用 unparkSuccessor,只唤醒队列中第一个有效等待者,其余继续挂起。
共享锁释放后调用 doReleaseShared,会循环尝试: - 唤醒一个后继节点 - 被唤醒的节点若再次调用 tryAcquireShared 返回正值,就继续唤醒下一个 - 直到资源耗尽或队列尾部
这种“传播式唤醒”让多个线程能快速并发进入,但也带来更高 CAS 开销和潜在过度唤醒风险。
立即学习“Java免费学习笔记(深入)”;
state 字段的语义完全不同
独占模式下,state 通常作二值开关或重入计数器: - 0 表示空闲 - >0 表示被某线程持有,数值即重入次数
共享模式下,state 表示可用资源数量: - 如 Semaphore 中代表剩余许可数 - CountDownLatch 中代表还需倒数的次数 - 每次获取成功就 CAS 减一,释放就加一
ReadWriteLock 更进一步,把 state 拆成高低 16 位分别记读锁和写锁计数,语义更复杂,但复用同一变量。
节点类型标记与队列共用但行为隔离
两种模式都往同一个 CLH 队列里插节点,但靠 nextWaiter 字段区分身份: - 独占节点设为 null(等价于 Node.EXCLUSIVE) - 共享节点设为 Node.SHARED 静态实例
AQS 内部方法(如 shouldParkAfterFailedAcquire、setHeadAndPropagate)会显式检查这个标记,从而走不同分支。这意味着: - 同一队列里可以混排读线程和写线程(如 ReadWriteLock) - 但写锁释放时不能误触发读线程的传播唤醒,必须靠位运算严格隔离状态位


















