AQS 原理详解:从 CLH 队列到 ReentrantLock 的实现

1. 引言

AQS(AbstractQueuedSynchronizer,抽象队列同步器)是 Java 并发包(java.util.concurrent)中最核心的基础组件之一。ReentrantLockSemaphoreCountDownLatchReentrantReadWriteLock 等同步工具,其底层实现都依赖于 AQS。

理解 AQS 是深入掌握 Java 并发编程的关键一步。本文将从 AQS 的设计思想出发,逐步剖析其核心数据结构、同步状态管理、CLH 队列的入队与出队流程,并结合 ReentrantLock 的源码,带你彻底搞懂 AQS 的工作原理。

2. AQS 是什么

AQS 是一个用于构建锁和同步器的框架。它提供了一个基于 FIFO(先进先出)等待队列的阻塞锁机制,以及一个表示同步状态的 volatile int state 字段。

使用 AQS 可以方便地实现自定义同步器,只需重写以下几个方法即可:

  • tryAcquire(int arg):独占式获取同步状态
  • tryRelease(int arg):独占式释放同步状态
  • tryAcquireShared(int arg):共享式获取同步状态
  • tryReleaseShared(int arg):共享式释放同步状态
  • isHeldExclusively():当前线程是否独占资源

AQS 的设计采用了模板方法模式,它定义了获取和释放同步状态的骨架,而将具体的获取/释放逻辑留给子类实现。

3. 核心数据结构

3.1 同步状态 state

AQS 内部维护了一个 volatile int state 字段,用于表示同步状态:

java 复制代码
private volatile int state;
  • 对于 ReentrantLockstate 表示锁被重入的次数;
  • 对于 Semaphorestate 表示剩余的许可数量;
  • 对于 CountDownLatchstate 表示还需要等待的计数。

state 的读写提供了三种方法:

java 复制代码
protected final int getState() { return state; }
protected final void setState(int newState) { state = newState; }
protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

其中 compareAndSetState 基于 CAS(Compare-And-Swap)操作,保证并发环境下状态更新的原子性。

3.2 CLH 队列节点 Node

AQS 内部使用一个 CLH 队列 (Craig, Landin, and Hagersten 锁队列的变体)来管理等待线程。队列中的每个节点都是一个 Node 对象:

java 复制代码
static final class Node {
    // 共享模式
    static final Node SHARED = new Node();
    // 独占模式
    static final Node EXCLUSIVE = null;

    // 等待状态:取消
    static final int CANCELLED = 1;
    // 等待状态:后继节点需要唤醒
    static final int SIGNAL = -1;
    // 等待状态:条件等待
    static final int CONDITION = -2;
    // 等待状态:共享式传播
    static final int PROPAGATE = -3;

    volatile int waitStatus;
    volatile Node prev;
    volatile Node next;
    volatile Thread thread;
    Node nextWaiter;
}

waitStatus 是节点的等待状态,初始值为 0。当节点被取消时置为 CANCELLED;当节点的后继线程需要被唤醒时置为 SIGNAL

4. 获取锁的流程

以独占模式为例,获取同步状态的入口方法是 acquire(int arg)

java 复制代码
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

整个流程分为三步:

  1. 尝试获取 :调用 tryAcquire(arg) 尝试获取同步状态,成功则直接返回;
  2. 入队 :获取失败,调用 addWaiter(Node.EXCLUSIVE) 将当前线程封装成节点加入等待队列尾部;
  3. 自旋等待 :调用 acquireQueued() 让节点在队列中自旋,直到获取到同步状态或被中断。

4.1 入队操作 addWaiter

java 复制代码
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

入队时先尝试快速入队:将新节点的 prev 指向当前尾节点,然后通过 CAS 将尾节点更新为新节点。如果 CAS 失败(说明有竞争),则进入 enq() 方法通过自旋 + CAS 的方式保证入队成功。

4.2 自旋等待 acquireQueued

java 复制代码
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null;
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

节点在队列中自旋,只有当它的前驱节点是头节点时,才会再次尝试获取锁。如果前驱不是头节点,或者获取失败,则调用 shouldParkAfterFailedAcquire 判断是否需要挂起当前线程。

5. 释放锁的流程

独占模式释放同步状态的入口方法是 release(int arg)

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) 尝试释放同步状态;
  2. 释放成功后,找到头节点,调用 unparkSuccessor(h) 唤醒头节点的后继节点。

5.1 唤醒后继节点 unparkSuccessor

java 复制代码
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        LockSupport.unpark(s.thread);
}

唤醒时,如果后继节点为 null 或已被取消(waitStatus > 0),则从尾节点向前遍历,找到最靠近头节点的有效节点进行唤醒。

6. ReentrantLock 的实现

ReentrantLock 是 AQS 最典型的应用。它内部定义了一个继承 AQS 的同步器 Sync,并分为公平锁 FairSync 和非公平锁 NonfairSync 两种实现。

6.1 非公平锁

java 复制代码
static final class NonfairSync extends Sync {
    final void lock() {
        // 先直接尝试 CAS 抢占一次
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

非公平锁在 lock() 时,会先尝试直接 CAS 抢占锁,如果成功则立即获得锁,不进入队列。这就是「非公平」的体现------新来的线程可能插队。

6.2 公平锁

java 复制代码
static final class FairSync extends Sync {
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            // 公平锁:先检查队列中是否有前驱节点
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

公平锁在获取锁之前,会先调用 hasQueuedPredecessors() 检查等待队列中是否有前驱节点。如果有,则当前线程必须排队,不能插队。

6.3 可重入的实现

ReentrantLock 的可重入性体现在 tryAcquire 中:当 state > 0 且当前线程是持有锁的线程时,state 累加,表示重入次数增加。释放时 state 递减,直到 state == 0 才真正释放锁。

7. 总结

AQS 是 Java 并发编程的基石,其核心设计可以概括为以下几点:

  • 一个状态volatile int state 表示同步状态;
  • 一个队列:CLH 变体队列管理等待线程;
  • 两种模式:独占模式与共享模式;
  • 三种操作:获取、释放、条件等待。

理解 AQS 的关键在于把握「模板方法模式 + CAS + 自旋 + 阻塞唤醒」的组合。掌握了 AQS,再去看 ReentrantLockSemaphoreCountDownLatch 等同步器的源码,就会豁然开朗。

相关推荐
Doubbbbbbble云3 小时前
区间合并问题的常见算法模式与优化思路4
java·数据结构·算法
持敬chijing3 小时前
Web前后端开发-JavaScript 入门教程
开发语言·前端·javascript
蜗牛互联网3 小时前
语音AI开始边听边说,改变的不只是响应速度
java·人工智能·后端·语音识别
2601_966949653 小时前
从轮询到策略消费:量化系统如何高效处理五档盘口数据
开发语言·python·数据分析·pandas·量化交易·股票数据·quantdash
卓怡学长3 小时前
w233基于vue和springboot的宠物管理系统的设计与实现
java·vue.js·spring boot·spring·intellij-idea
我命由我123454 小时前
人脸识别 - 去重时间窗口
java·开发语言·python·学习·java-ee·学习方法·python3.11
逆境不可逃4 小时前
Pi Agent 学习笔记:一次提问背后的工具调用循环
java·笔记·学习
落魄大学生之流水线上谋生计4 小时前
Java并发核心机制详解:线程池、CAS、AQS、锁升级
java·开发语言·数据库
hahaha60164 小时前
HLS高层次综合设计技巧--数组
开发语言·嵌入式硬件·算法·fpga开发