【锁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 的灵魂,含义由子类定义。

  • ReentrantLockstate 表示锁的重入次数(0=无锁,>0=有锁)。
  • Semaphorestate 表示剩余的许可证数量。
  • CountDownLatchstate 表示计数器当前的值。

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 初始状态 刚加入队列时的默认值。

关键逻辑

  • 只有前驱节点的 waitStatusSIGNAL 时,当前节点才能安全地 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. 将该节点转移到同步队列 尾部(状态改为 SIGNAL0)。
  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 并发包半壁江山。

相关推荐
AI砖家1 小时前
多商户多租户系统架构设计文档(Java版)
java·开发语言·系统架构·多租户·多商户
落苜蓿蓝1 小时前
Java 循环中对象复用导致属性覆盖?从 JVM 内存模型讲解原因
java·jvm·python
weixin_440784112 小时前
Android基础知识汇总
android·java·android studio
布鲁飞丝2 小时前
从零实现富文本编辑器#-浏览器选区与编辑器选区模型同步
java·前端·编辑器
jun_bai2 小时前
使用java安全的移动文件
java
caishenzhibiao2 小时前
市场同步系统 同花顺期货通指标
java·c语言·c#
Devin~Y2 小时前
从本地生活电商到 AI RAG:互联网大厂 Java 面试场景完整实战
java·spring boot·redis·elasticsearch·spring cloud·kafka·rag
520拼好饭被践踏2 小时前
JAVA+Agent学习day25
java·开发语言·学习
大黄说说2 小时前
Java 并发大坑:volatile、synchronized、Lock 三者如何选择?
java·开发语言