文章目录

Java AQS独占锁与共享锁的实现差异

发布于 2026-04-21 02:20:08 · 浏览 9 次 · 评论 0 条

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) 方法中的自旋逻辑。

  1. 获取 当前节点的前驱节点 p
  2. 判断 前驱节点是否为头节点(head)。如果是,说明当前节点有机会获取锁。
  3. 尝试 再次调用 tryAcquireShared(arg) 获取锁。
    • 若返回值 r >= 0,说明获取成功。
    • 调用 setHeadAndPropagate(node, r)。这一步不仅将当前节点设为头节点,还会根据返回值决定是否唤醒后继节点。
  4. 若前驱节点不是头节点,或获取锁失败,则 检查 前驱节点的 waitStatus 是否为 SIGNAL,若是,则 挂起 当前线程。

4. 深究唤醒机制与传播行为(核心差异)

这是独占锁与共享锁最本质的区别。对比 两者的唤醒流程。

graph TD A[头节点释放锁] --> B{锁类型?} B -- 独占锁 --> C[调用 unparkSuccessor] C --> D[唤醒后继节点] D --> E[后继节点获取锁成为新头节点] E --> F[唤醒流程结束] B -- 共享锁 --> G[调用 doReleaseShared] G --> H[唤醒后继节点] H --> I[后继节点获取锁] I --> J[调用 setHeadAndPropagate] J --> K{是否需要传播?} K -- 是 --> L[继续调用 doReleaseShared] L --> H K -- 否 --> F

解读 独占锁的唤醒行为:

  • 当持有锁的线程 执行 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 时所有等待线程被唤醒。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文