AQS(AbstractQueuedSynchronizer)核心原理探索

在 Java 并发编程的世界里,AbstractQueuedSynchronizer(简称 AQS)是一座隐藏的基石。从ReentrantLock到CountDownLatch,从Semaphore到ThreadPoolExecutor,诸多并发工具的实现都依赖于 AQS 的核心机制。AQS 通过巧妙的队列管理和状态控制,为开发者提供了构建同步器的通用框架。本文将深入 AQS 的内部世界,解析其核心结构、同步队列的运作机制及状态控制逻辑,揭开 Java 并发工具的底层奥秘。

一、AQS 的设计定位:同步器的通用骨架

AQS 是一个抽象类,其设计目标是为各种同步器提供基础框架。它封装了同步状态的管理、线程的排队等待、唤醒等核心操作,开发者只需重写少量方法即可实现自定义同步器。

1.1 核心设计思想

AQS 的核心思想可以概括为 **“状态控制 + 队列管理”**:

  • 状态控制:通过一个 volatile 修饰的 int 变量(state)表示同步状态,子类通过getState()、setState()、compareAndSetState()等方法操作状态;
  • 队列管理:当线程获取同步状态失败时,AQS 会将线程封装为节点并加入同步队列,等待被唤醒后重新尝试获取状态。

这种设计将 “实现同步” 的共性逻辑(如排队、唤醒)与 “判断是否允许访问” 的个性逻辑(如锁的获取条件)分离,极大简化了同步器的实现。

1.2 核心方法与模板模式

AQS 通过模板模式定义了同步操作的骨架,子类需重写以下关键方法(默认抛出UnsupportedOperationException):

方法

功能描述

protected boolean tryAcquire(int arg)

独占式获取同步状态,返回true表示成功

protected boolean tryRelease(int arg)

独占式释放同步状态,返回true表示成功

protected int tryAcquireShared(int arg)

共享式获取同步状态,返回值≥0 表示成功

protected boolean tryReleaseShared(int arg)

共享式释放同步状态,返回true表示后续线程可获取

protected boolean isHeldExclusively()

判断当前线程是否独占同步状态

AQS 提供的模板方法(如acquire()、release()、acquireShared()等)会调用上述方法,实现完整的同步逻辑。例如,ReentrantLock通过重写tryAcquire()和tryRelease()实现独占锁,CountDownLatch通过重写tryAcquireShared()和tryReleaseShared()实现共享同步。

二、AQS 的内部结构:状态与队列的协作

AQS 的内部结构主要包括同步状态(state)同步队列(CLH 队列) 两部分,二者的协作是实现同步的关键。

2.1 同步状态(state)的设计

AQS 用一个 volatile 变量state存储同步状态,其含义由子类定义:

  • 在ReentrantLock中,state表示锁的重入次数(0 表示未锁定,≥1 表示已锁定);
  • 在Semaphore中,state表示可用许可的数量;
  • 在CountDownLatch中,state表示计数器的初始值。

状态操作的线程安全性:AQS 通过Unsafe类的 CAS 操作保证state的原子性,例如compareAndSetState(int expect, int update)方法会原子性地将state从expect更新为update,失败则返回false。

// AQS中state的定义与操作
private volatile int state;

protected final int getState() {
    return state;
}

protected final void setState(int newState) {
    state = newState;
}

protected final boolean compareAndSetState(int expect, int update) {
    // 调用Unsafe的CAS操作
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

2.2 同步队列(CLH 队列)的结构

当线程获取同步状态失败时,AQS 会将线程封装为节点(Node) 并加入同步队列。该队列是一个双向链表,基于 CLH(Craig, Landin, and Hagersten)锁队列改进而来,特点是FIFO(先进先出)自旋等待

2.2.1 节点(Node)的结构

每个节点包含以下核心字段:

static final class Node {

// 节点状态:CANCELLED(1)、SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3)、0(初始)
volatile int waitStatus;

// 前驱节点
volatile Node prev;

// 后继节点
volatile Node next;

// 关联的线程
volatile Thread thread;

// 条件队列中的后继节点(用于Condition)
Node nextWaiter;

}

节点状态(waitStatus)的含义

  • CANCELLED(1):节点已取消(如超时或被中断),不再参与竞争;
  • SIGNAL(-1):后继节点需要被唤醒,当前节点释放锁时需唤醒后继;
  • CONDITION(-2):节点处于条件队列中,等待被唤醒;
  • PROPAGATE(-3):共享模式下,状态需向后传播(如 CountDownLatch);
  • 0:初始状态,无特殊含义。
2.2.2 队列的头节点与尾节点

AQS 通过head和tail指针维护同步队列:

  • head:头节点,表示当前持有锁的线程(或已成功获取状态的线程);
  • tail:尾节点,新节点会被加入到尾部。
// AQS中队列的头、尾指针
private transient volatile Node head;
private transient volatile Node tail;

队列初始化:队列初始时为空(head和tail均为null)。当第一个线程获取状态失败时,会创建头节点和尾节点(头节点为哨兵节点,不关联线程),后续节点通过 CAS 操作加入队列尾部。

三、独占式同步:获取与释放的完整流程

独占式同步是指同一时间只有一个线程能获取同步状态(如ReentrantLock)。AQS 通过acquire(int arg)(获取)和release(int arg)(释放)方法实现独占式操作。

3.1 独占式获取(acquire)流程

acquire(int arg)的逻辑可概括为:尝试获取状态→失败则入队→等待唤醒后重试,具体步骤如下:  

  1. 尝试获取同步状态:调用tryAcquire(arg),若返回true,则获取成功,直接返回;
  2. 获取失败,封装为节点入队
    • 将当前线程封装为Node.EXCLUSIVE(独占模式)节点;
    • 通过addWaiter(Node mode)方法将节点加入队列尾部(CAS 操作保证原子性);

 

3.阻塞等待:调用acquireQueued(Node node, int arg),节点进入自旋状态:

    • 若当前节点的前驱是头节点,再次尝试tryAcquire(arg);
    • 若获取成功,将当前节点设为新头节点,返回;
    • 若前驱节点状态为SIGNAL,则通过LockSupport.park(this)阻塞当前线程;

 

4.唤醒后处理:线程被唤醒后,重复步骤 3,直至获取状态成功或被中断。

核心代码片段

public final void acquire(int arg) {

    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
        selfInterrupt(); // 若被中断,记录中断状态
    }

}
3.1.1 入队操作(addWaiter)

addWaiter方法通过 CAS 操作将节点加入队列尾部,若尾节点为null(队列未初始化),则先初始化队列:

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)) { // CAS设置新尾节点
            pred.next = node;
            return node;
        }
    }
    enq(node); // 队列未初始化或CAS失败,通过enq初始化并入队
    return node;
}

private Node enq(final Node node) {
    for (;;) { // 自旋确保入队成功
        Node t = tail;
        if (t == null) { // 初始化队列,创建头节点
            if (compareAndSetHead(new Node())) {
                tail = head;
            }
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

3.2 独占式释放(release)流程

release(int arg)的逻辑是:尝试释放状态→成功则唤醒后继节点,具体步骤如下:

  1. 尝试释放同步状态:调用tryRelease(arg),若返回true,则释放成功;
  1. 唤醒后继节点
    • 获取头节点,若头节点不为null且状态不为 0,调用unparkSuccessor(Node node)唤醒后继节点;
    • 后继节点被唤醒后,会重新尝试获取同步状态。

核心代码片段

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0) {
            unparkSuccessor(h); // 唤醒后继节点
        }
        return true;
    }
    return false;
}

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); // 唤醒线程
    }
}

四、共享式同步:多线程共享资源的实现​

共享式同步允许多个线程同时获取同步状态(如CountDownLatch、Semaphore)。AQS 通过acquireShared(int arg)(获取)和releaseShared(int arg)(释放)方法实现共享式操作。​

4.1 共享式获取(acquireShared)流程​

acquireShared(int arg)的逻辑与独占式类似,但允许多个线程同时成功:​

  1. 尝试获取共享状态:调用tryAcquireShared(arg),返回值≥0 表示成功,<0 表示失败;​
  1. 失败则入队:通过doAcquireShared(arg)将线程封装为Node.SHARED节点入队;​
  1. 等待唤醒:节点进入自旋,若前驱是头节点,再次尝试获取;若成功,唤醒后续共享节点(通过setHeadAndPropagate传播状态);若失败,则阻塞等待。​

核心代码片段:​

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0) {
        doAcquireShared(arg);
    }
}

private void doAcquireShared(int arg) {
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) { // 获取成功
                    setHeadAndPropagate(node, r); // 设为头节点并传播状态
                    p.next = null; // 帮助GC
                    if (interrupted) {
                        selfInterrupt();
                    }
                    failed = false;
                    return;
                }
            }
            // 阻塞当前线程,直至被唤醒
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt()) {
                interrupted = true;
            }
        }
    } finally {
        if (failed) {
            cancelAcquire(node);
        }
    }
}

状态传播(setHeadAndPropagate):共享模式下,当前节点获取状态成功后,需唤醒后续共享节点(如CountDownLatch计数器归 0 后,所有等待线程都应被唤醒)。​

4.2 共享式释放(releaseShared)流程​

releaseShared(int arg)的逻辑是:尝试释放状态→成功则唤醒后继节点,且可能唤醒多个节点(与独占式不同):​

  1. 尝试释放共享状态:调用tryReleaseShared(arg),返回true表示释放成功;​
  1. 唤醒后继节点:通过doReleaseShared()唤醒队列中的节点,且会传播唤醒(即唤醒一个节点后,该节点可能继续唤醒下一个)。​

核心代码片段:​

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) { // 后继节点需要唤醒
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) {
                    continue; // CAS失败,重试
                }
                unparkSuccessor(h); // 唤醒后继
            } else if (ws == 0 &&
                       !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) {
                continue; // 设为PROPAGATE,确保状态传播
            }
        }
        if (h == head) { // 头节点未变化,退出循环
            break;
        }
    }
}

五、条件队列:线程间协作的补充机制

AQS 通过条件队列(Condition) 支持线程间的协作,类似于synchronized中的wait()/notify(),但更灵活(支持多个条件队列)。Condition的实现依赖于 AQS 的节点结构,与同步队列形成互补。

5.1 条件队列的结构

每个Condition对象对应一个单向链表的条件队列,节点类型为Node.CONDITION。当线程调用Condition.await()时,会从同步队列转移到条件队列并阻塞;当调用Condition.signal()时,会将条件队列的节点转移到同步队列,等待获取同步状态。

核心操作

  • await():释放同步状态,将线程加入条件队列并阻塞;
  • signal():将条件队列的头节点转移到同步队列,等待唤醒;
  • signalAll():将条件队列的所有节点转移到同步队列。

5.2 与同步队列的交互

条件队列与同步队列的交互是线程协作的关键:

  1. 线程获取同步状态后,调用await():释放状态→加入条件队列→阻塞;
  2. 其他线程调用signal():将条件队列节点转移到同步队列→唤醒线程;
  3. 被唤醒的线程在同步队列中重新尝试获取状态,成功后继续执行。

例如,ReentrantLock的newCondition()方法会创建一个ConditionObject(AQS 的内部类),实现条件等待功能。

代码示例:Condition 的 await 与 signal

public class ConditionExample {
    private final Lock lock = new ReentrantLock();
    private final Condition condition = lock.newCondition();
    private boolean flag = false;

    public void waitForFlag() throws InterruptedException {
        lock.lock();
        try {
            while (!flag) {
                condition.await(); // 加入条件队列并阻塞
            }
            System.out.println("Flag is true, continue working");
        } finally {
            lock.unlock();
        }
    }

    public void setFlag() {
        lock.lock();
        try {
            flag = true;
            condition.signal(); // 将条件队列节点转移到同步队列
        } finally {
            lock.unlock();
        }
    }
}

在上述示例中,调用waitForFlag()的线程会因flag为false进入条件队列;当setFlag()被调用后,flag置为true,条件队列的线程被转移到同步队列,等待获取锁后继续执行。

六、AQS 的应用:并发工具的底层依赖

AQS 是 Java 并发工具的 “基因”,理解 AQS 能帮助我们更深入地掌握这些工具的工作原理。

6.1 ReentrantLock 与 AQS

ReentrantLock通过重写 AQS 的独占式方法实现:

  • tryAcquire(int arg):通过 CAS 尝试获取锁,支持重入(state+1)和公平性判断;
    • 公平锁:获取前先检查队列中是否有前驱节点,无则尝试 CAS;
    • 非公平锁:直接尝试 CAS,失败后再入队;
  • tryRelease(int arg):释放锁(state-1),当state为 0 时表示完全释放,返回true。

公平锁与非公平锁的性能差异:非公平锁因省去队列检查步骤,在高并发场景下吞吐量更高,但可能导致线程饥饿;公平锁虽保证顺序,但因频繁的队列操作会增加开销。

6.2 CountDownLatch 与 AQS

CountDownLatch通过重写 AQS 的共享式方法实现:

  • 初始化时,state设为计数器值(如new CountDownLatch(3)则state=3);
  • tryAcquireShared(int arg):判断state是否为 0,是则返回 0(成功),否则返回 - 1(失败);
  • tryReleaseShared(int arg):通过 CAS 将state减 1,当state变为 0 时返回true,触发所有等待线程唤醒。

典型场景:主线程等待多个子线程完成初始化,子线程全部执行countDown()后,主线程从await()返回。

6.3 Semaphore 与 AQS

Semaphore(信号量)用于控制同时访问资源的线程数量,其 AQS 实现逻辑为:

  • state表示可用许可数量;
  • 非公平模式tryAcquireShared(int arg):直接 CAS 减少state,成功则返回剩余许可;
  • 公平模式tryAcquireShared(int arg):先检查队列,无等待线程再 CAS 减少state;
  • tryReleaseShared(int arg):CAS 增加state,释放许可。

示例:Semaphore(5)允许 5 个线程同时获取许可,第 6 个线程需等待其他线程释放许可。

七、AQS 的设计智慧与局限

7.1 设计亮点

  • 模板模式的极致应用:将共性逻辑(排队、唤醒)与个性逻辑(状态判断)分离,极大降低同步器实现难度;
  • 高效的队列管理:基于 CLH 队列的双向链表设计,结合 CAS 操作实现无锁入队,减少线程阻塞;
  • 多模式支持:同时支持独占式和共享式,满足不同同步场景需求;
  • 内存可见性保证:state的 volatile 修饰与 CAS 操作,确保线程间状态的可见性与原子性。

7.2 局限性

  • 单一状态变量:state为 int 类型,复杂同步器可能需要多个状态,需通过位运算拆分(如ReentrantReadWriteLock用state高 16 位表示读锁,低 16 位表示写锁);
  • 线程唤醒的不确定性:唤醒操作依赖LockSupport.unpark(),但线程何时被调度由操作系统决定,可能存在延迟;
  • 自定义难度高:需深入理解 AQS 机制才能正确实现同步器,否则易出现死锁、饥饿等问题。

八、总结:AQS 在并发体系中的地位

AQS 是 Java 并发编程的 “基础设施”,它以简洁而强大的设计支撑了众多并发工具的实现。其核心价值在于:

  • 抽象共性:将同步器的通用逻辑(排队、唤醒、状态管理)抽象为模板方法,避免重复开发;
  • 灵活扩展:通过状态变量和钩子方法,允许子类根据需求定制同步规则;
  • 性能优化:基于 CAS 和自旋的无锁操作,减少线程上下文切换开销。

理解 AQS 不仅是掌握ReentrantLock、CountDownLatch等工具的关键,更是深入领会 Java 并发编程思想的必经之路。它的设计理念 ——用状态控制访问权限,用队列管理等待线程—— 为解决复杂并发问题提供了通用思路,也为自定义同步器开发奠定了坚实基础。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

练习时长两年半的程序员小胡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值