Java AQS独占锁与共享锁的实现差异
Java 并发包(JUC)中的 AbstractQueuedSynchronizer(AQS)是构建锁和同步器的基础框架。AQS 内部主要维护了一个由双向链表组成的同步队列,并使用一个 volatile int state 变量来表示同步状态。理解独占锁与共享锁的实现差异,关键在于掌握它们对 state 的定义、节点入队的标记方式以及锁释放后的唤醒传播机制。
1. 理解核心概念与资源访问模式
区分 两种锁的并发访问策略。
- 独占锁:采用悲观策略。同一时刻只允许一个线程持有锁。当锁被占用时,其他尝试获取的线程必须进入同步队列等待。
- 共享锁:采用乐观策略。同一时刻允许多个线程同时持有锁。多个线程可以并发访问共享资源。
识别 常见的实现类。
| 锁类型 | 实现类 | 特性描述 |
|---|---|---|
| 独占锁 | ReentrantLock |
支持可重入,可指定公平或非公平策略 |
| 独占锁 | synchronized |
JVM 内置锁,关键字实现,隐式加锁/解锁 |
| 共享锁 | Semaphore (permits > 1) |
信号量,控制同时访问的线程数量 |
| 共享锁 | CountDownLatch |
倒计时计数器,允许一组线程等待另一组线程 |
| 共享锁 | ReentrantReadWriteLock.ReadLock |
读写锁中的读锁,多个读操作可共存 |
2. 剖析 AQS 队列中的节点模式
AQS 中的每一个等待线程都被封装成一个 Node 节点。查看 源码可知,节点通过 nextWaiter 属性区分模式。
- 独占模式:节点被标记为
Node.EXCLUSIVE。 - 共享模式:节点被标记为
Node.SHARED。
实现 独占锁获取时,AQS 模板方法会 调用 tryAcquire(int arg)。子类(如 ReentrantLock)需 重写 该方法,利用 CAS 修改 state 从 0 变为 1(或重入计数加 1)。如果修改失败,线程将 封装 为 Node.EXCLUSIVE 节点并 加入 同步队列尾部。
实现 共享锁获取时,AQS 模板方法会 调用 tryAcquireShared(int arg)。子类(如 Semaphore)需 重写 该方法,通常利用 CAS 修改 state 减小剩余许可数。如果剩余资源不足,线程将 封装 为 Node.SHARED 节点并 加入 同步队列尾部。
3. 掌握锁获取与状态判断逻辑
分析 acquireShared 的核心源码逻辑。与独占锁直接阻塞不同,共享锁在获取失败时会有特殊的判断逻辑。
public final void acquireShared(int arg) {
// tryAcquireShared 返回值 >= 0 表示获取成功
// 返回值 < 0 表示获取失败,需执行 doAcquireShared 进入队列
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
执行 doAcquireShared(int arg) 方法中的自旋逻辑。
- 获取 当前节点的前驱节点
p。 - 判断 前驱节点是否为头节点(
head)。如果是,说明当前节点有机会获取锁。 - 尝试 再次调用
tryAcquireShared(arg)获取锁。- 若返回值
r >= 0,说明获取成功。 - 调用
setHeadAndPropagate(node, r)。这一步不仅将当前节点设为头节点,还会根据返回值决定是否唤醒后继节点。
- 若返回值
- 若前驱节点不是头节点,或获取锁失败,则 检查 前驱节点的
waitStatus是否为SIGNAL,若是,则 挂起 当前线程。
4. 深究唤醒机制与传播行为(核心差异)
这是独占锁与共享锁最本质的区别。对比 两者的唤醒流程。
解读 独占锁的唤醒行为:
- 当持有锁的线程 执行
release()操作后,头节点会 唤醒 队列中的第一个后继节点。 - 被唤醒的节点 获取 锁成功后,将自己 设置 为新的头节点。
- 关键点:新的头节点仅仅负责执行自己的任务,不会 主动去唤醒它的后继节点。唤醒工作必须等待该线程显式 调用
release()触发。这保证了独占性,同一时间只有一个线程在运行。
解读 共享锁的唤醒行为(传播机制):
- 当头节点 执行
releaseShared()时,会 调用doReleaseShared()唤醒 后继节点。 - 后继节点被唤醒并 执行
setHeadAndPropagate方法成为新头节点时,不仅设置自己为头节点,还会 检查propagate参数(通常表示剩余资源量)或旧的waitStatus。 - 关键点:如果满足条件(例如
propagate > 0),新的头节点会立即 调用doReleaseShared(),继续 唤醒 它的下一个后继节点。 - 这种机制使得共享锁的状态能够“向后传播”,多个等待的共享线程可以连续被唤醒并执行,无需每次都等待前一个线程显式释放。
5. 对比关键源码方法
对比 独占锁与共享锁在 AQS 中涉及的核心方法签名与作用。
| 方法名 | 锁模式 | 作用描述 |
|---|---|---|
tryAcquire(int arg) |
独占 | 尝试获取锁,由子类实现,通常使用 CAS 修改 state |
acquire(int arg) |
独占 | 获取锁的入口,若失败则将线程加入队列阻塞 |
release(int arg) |
独占 | 释放锁,唤醒后继节点 |
tryAcquireShared(int arg) |
共享 | 尝试获取共享资源,返回剩余资源数 |
acquireShared(int arg) |
共享 | 获取共享锁入口,若失败则加入队列阻塞 |
releaseShared(int arg) |
共享 | 释放共享锁,并触发唤醒传播 |
setHeadAndPropagate(Node, int) |
共享 | 设置头节点并根据状态决定是否传播唤醒 |
注意 state 变量的含义差异。
- 在
ReentrantLock(独占)中,state为 0 表示无锁,大于 0 表示重入次数。 - 在
Semaphore(共享)中,state表示剩余的许可数量。 - 在
CountDownLatch(共享)中,state表示还需要倒计数的次数,减至 0 时所有等待线程被唤醒。

暂无评论,快来抢沙发吧!