【锁6】AQS (AbstractQueuedSynchronizer) 核心原理详解

AQS (AbstractQueuedSynchronizer) 核心原理详解

AQS 是 Java 并发包的基石 。它提供了一个基于 FIFO 等待队列 的框架,用于构建锁或其他同步器(如 Semaphore、CountDownLatch)。

一句话概括 :AQS 用一个 volatile int state 表示资源状态,用一个 CLH 队列 存放等待资源的线程,通过 CAS 修改状态,实现了线程间的阻塞与唤醒管理。


一、 AQS 的设计目标与地位

1. 为什么要 AQS?

在没有 AQS 之前,开发者需要自己处理:

  • 线程阻塞/唤醒(park/unpark)
  • 线程排队(队列维护)
  • 原子状态切换(CAS)

AQS 将这些共性逻辑 抽象出来,让开发者只需关注 "资源是否可用" 这一业务逻辑。

2. AQS 家族谱

AQS 是 JUC(java.util.concurrent)的骨架。

bash 复制代码
java.util.concurrent.locks

├── AbstractQueuedSynchronizer (AQS)

│   ├── ReentrantLock (Sync -> NonfairSync / FairSync)

│   ├── ReentrantReadWriteLock (Sync -> ReadLock / WriteLock)

│   ├── Semaphore (Sync -> NonfairSync / FairSync)

│   └── CountDownLatch (Sync)

└── LockSupport (底层工具类,用于阻塞/唤醒线程)

二、 AQS 的两大核心

1. 同步状态(volatile int state)

state 是 AQS 的灵魂,含义由子类定义。

  • ReentrantLock :state 表示锁的重入次数(0=无锁,>0=有锁)。
  • Semaphore :state 表示剩余的许可证数量。
  • CountDownLatch :state 表示计数器当前的值。

AQS 提供了三种原子操作来访问 state:

  • getState()
  • setState()
  • compareAndSetState()(核心,基于 CAS 实现)

2. CLH 队列(FIFO 等待队列)

AQS 内部维护了一个 双向链表队列(改良版的 Craig, Landin, and Hagersten 锁队列)。

  • 作用 :存放那些获取资源失败的线程。
  • 节点(Node) :队列中的每个元素都是一个 Node。
    • thread:封装的等待线程。
    • prev / next:前后指针。
    • waitStatus:节点状态(极其重要,见下文)。
    • nextWaiter:用于 Condition 条件队列。

三、 Node 节点的 waitStatus 详解

waitStatus 决定了节点的命运。

状态值 含义 说明
CANCELLED (1) 取消 线程因超时或中断被取消,不会再参与竞争。
SIGNAL (-1) 唤醒后继 当前节点的后继节点被(或即将被)阻塞,当前节点释放锁或被取消时必须唤醒后继节点。
CONDITION (-2) 条件等待 节点在 Condition 队列中,等待被 signal() 转移到同步队列。
PROPAGATE (-3) 传播 (共享模式)确保唤醒能够向后传播(用于 ReadLock)。
0 初始状态 刚加入队列时的默认值。

关键逻辑:

  • 只有前驱节点的 waitStatus 是 SIGNAL 时,当前节点才能安全地 park(阻塞)。
  • 如果前驱节点是 CANCELLED,需要从队列中移除。

四、 AQS 的工作模式

AQS 定义了两种资源获取模式:

1. 独占模式 (Exclusive)

同一时间只有一个线程能持有资源。

  • 典型实现:ReentrantLock
  • 核心方法:
    • acquire(int arg):获取资源(独占)
    • release(int arg):释放资源(独占)

2. 共享模式 (Shared)

多个线程可以同时持有资源。

  • 典型实现:Semaphore, CountDownLatch, ReadLock
  • 核心方法:
    • acquireShared(int arg):获取资源(共享)
    • releaseShared(int arg):释放资源(共享)

五、 AQS 核心流程源码解析(独占模式)

1. 获取锁:acquire()

java 复制代码
public final void acquire(int arg) {

if (!tryAcquire(arg) && // ① 尝试直接获取(子类实现)

acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // ② 获取失败,入队并自旋

selfInterrupt();

}

流程拆解:

  1. tryAcquire(arg) :
    • 这是 AQS 留给子类实现的钩子方法。
    • 在 ReentrantLock 中,它负责 CAS 修改 state 并检查重入。
    • 如果成功,线程直接返回,不进入队列。
  2. addWaiter(Node.EXCLUSIVE) :
    • 将当前线程包装成 Node.EXCLUSIVE 节点。
    • 通过 CAS 自旋,将节点追加到队列尾部。
  3. acquireQueued(node, arg) :
    • 节点在队列中"休息",等待被唤醒。
    • 自旋检查 :如果前驱节点是 Head,再次尝试 tryAcquire。
    • 阻塞 :如果前驱节点状态是 SIGNAL,调用 LockSupport.park() 挂起线程。
    • 清理 :检测到线程中断或超时,将节点状态设为 CANCELLED 并移除。

2. 释放锁:release()

java 复制代码
public final boolean release(int arg) {

if (tryRelease(arg)) { // ① 尝试释放(子类实现)

Node h = head;

if (h != null && h.waitStatus != 0)

unparkSuccessor(h); // ② 唤醒后继节点

return true;

}

return false;

}

流程拆解:

  1. tryRelease(arg) :
    • 钩子方法。ReentrantLock 在此将 state 减 1。
    • 只有当 state 归零时,才返回 true(完全释放)。
  2. unparkSuccessor(h) :
    • 找到头结点的下一个节点(即等待最久的线程)。
    • 调用 LockSupport.unpark(thread) 唤醒它。
    • 被唤醒的线程回到 acquireQueued 方法中,继续自旋尝试获取锁。

六、 Condition 的实现机制

AQS 内部类 ConditionObject 实现了 Condition 接口。

1. 两个队列

AQS 维护两个队列:

  1. 同步队列(CLH 队列):争抢锁的队列。
  2. 条件队列 (单向链表):调用 await() 后等待的队列。

2. await() 流程

  1. 线程调用 condition.await()。
  2. 释放持有的锁(state 归零)。
  3. 将线程包装成 Node 放入 条件队列 (状态为 CONDITION)。
  4. 调用 LockSupport.park() 挂起线程。

3. signal() 流程

  1. 线程调用 condition.signal()。
  2. 从 条件队列 头部取出一个节点。
  3. 将该节点转移到同步队列 尾部(状态改为 SIGNAL 或 0)。
  4. 当持有锁的线程调用 unlock() 时,会唤醒同步队列中的这个节点。
  5. 该节点在同步队列中再次争夺锁。

七、 模板方法模式(核心设计思想)

AQS 采用了典型的 模板方法模式 。它实现了同步状态的管理逻辑 (排队、阻塞、唤醒),但将具体的资源获取/释放规则留给子类实现。

子类必须实现的方法(钩子):

方法 模式 说明
tryAcquire(int) 独占 尝试获取资源
tryRelease(int) 独占 尝试释放资源
tryAcquireShared(int) 共享 尝试获取资源
tryReleaseShared(int) 共享 尝试释放资源
isHeldExclusively() 独占 是否被当前线程独占

示例:实现一个简单的互斥锁

java 复制代码
class Mutex implements Lock {

    private static class Sync extends AbstractQueuedSynchronizer {

        @Override

        protected boolean tryAcquire(int arg) {

            return compareAndSetState(0, 1); // CAS 抢锁

        }

        @Override

        protected boolean tryRelease(int arg) {

            setState(0); // 释放锁

            return true;

        }

    }

    private final Sync sync = new Sync();

    public void lock() {
        sync.acquire(1);
    }

    public void unlock() {
        sync.release(1);
    }

// ... 其他方法省略

}

八、 AQS 流程图(文字版)

获取锁(acquire)

bash 复制代码
开始

|

v

tryAcquire() --成功--> 结束

|

失败

|

v

addWaiter() --> 入队

|

v

acquireQueued()

|

+--> 前驱是Head? --是--> tryAcquire() --成功--> 出队,设为Head

|                       |

|                      失败

|                       |

|                       v

|                   park() 挂起

|

否

|

v

检查前驱状态,必要时park()

释放锁(release)

bash 复制代码
开始

|

v

tryRelease() --失败--> 结束

|

成功

|

v

unparkSuccessor()

|

v

唤醒后继节点

九、 总结(面试版)

  1. 核心结构 :AQS = volatile int state + CLH 双向队列。
  2. 资源共享 :支持 独占 (ReentrantLock)和 共享(Semaphore/CountDownLatch)两种模式。
  3. 底层实现 :使用 CAS 修改状态,使用 LockSupport.park/unpark 阻塞/唤醒线程。
  4. 排队机制:获取资源失败的线程被封装成 Node 进入队列,等待前驱节点唤醒。
  5. 设计模式 :模板方法模式,子类只需实现 tryAcquire/tryRelease 等钩子方法。
  6. Condition :AQS 通过维护独立的 条件队列 ,实现了精准的线程等待/唤醒机制,比 synchronized + wait/notify 更灵活。

一句话升华:

AQS 屏蔽了底层同步机制的复杂性,通过一套统一的框架,撑起了 Java 并发包半壁江山。

相关推荐
阿俊-全栈开发10 小时前
LikeShop 单商户SaaS商城分销模块二开:佣金计算与结算逻辑实现
java·开发语言·微服务·likeshop·likeshop开源商城·likeshop多商户
wuminyu13 小时前
C++协程实现接收端的零拷贝Buffer管理原理剖析
java·linux·c语言·jvm·c++
蜗牛互联网15 小时前
WSL Containers GA:本地AI容器的生命周期、网络与治理验收
java·网络·人工智能·后端
龙亘川16 小时前
铭记英烈守初心,数字强基促服务:亘川智城退役军人服务系统的实践与价值
java·数据库·人工智能
OxYGC16 小时前
[AI工程] Spring AI 第廿一篇:存量 REST 接口的 MCP 化改造实录——工具从哪条路径注册、参数描述怎么写、身份怎么过去
java·人工智能·spring
Zhou14113616 小时前
SpringBoot_02_自动配置原理
java·spring boot·后端
FYKJ_201016 小时前
SSM校园失物招领系统41452-计算机课程设计、毕业设计
java·vue.js·spring boot·python·mysql·typescript·spark
kv11017 小时前
Android studio国内开发有关
java·数据库·android studio
卓怡学长17 小时前
w198基于springboot始于足下健康打卡平台的设计与实现
java·spring boot·spring·intellij-idea
阿狗童鞋17 小时前
Java并发编程实战指南
java·开发语言