AQS模型详解:Java并发的核心同步框架(从原理到实战)

在Java并发编程中,我们经常使用ReentrantLock、Semaphore、CountDownLatch等同步工具,却很少思考这些工具背后的"统一底层支撑"------它们本质上都是基于**AbstractQueuedSynchronizer(简称AQS)**实现的。AQS是Java并发包(java.util.concurrent.locks)的核心骨架,定义了一套通用的同步器框架,封装了线程排队、锁获取与释放、中断响应等核心逻辑,让开发者无需重复实现复杂的同步机制,就能快速构建安全高效的同步工具。

本文将从AQS的核心定位出发,逐步拆解其底层结构、核心机制、两种工作模式,结合源码片段和实战案例,详细讲解AQS的工作原理,让你不仅能"知其然",更能"知其所以然",彻底搞懂Java并发同步的底层逻辑。

一、先搞懂:AQS是什么,为什么需要它?

1. AQS的核心定位

AQS的全称是AbstractQueuedSynchronizer,翻译为"抽象队列同步器",它是一个抽象类,本身不直接实现同步功能,而是提供了一套同步器的"模板骨架"------定义了线程竞争资源的核心流程(排队、阻塞、唤醒),将具体的"资源获取/释放逻辑"交给子类去实现。

简单来说,AQS的核心作用是:统一管理线程的排队和调度,封装通用同步逻辑,简化同步工具的开发。如果没有AQS,ReentrantLock、Semaphore等工具都需要各自实现线程排队、阻塞唤醒等逻辑,会导致代码冗余、效率低下,且难以保证线程安全。

2. 为什么需要AQS?

在并发编程中,线程之间的竞争与协作是核心问题,而同步工具的核心需求的是"控制线程对资源的访问权限"。如果没有统一的框架,开发者实现同步逻辑时会面临两个核心难题:

  • 线程竞争无序:多个线程同时竞争资源时,容易出现"插队"现象,导致公平性无法保证,甚至出现死锁;

  • 线程调度低效:线程获取资源失败后,若一直自旋重试,会浪费大量CPU资源;若直接终止,又无法保证后续能重新获取资源。

AQS通过"状态管理+队列调度"的组合,完美解决了这两个问题:用一个volatile状态变量控制资源访问权限,用一个FIFO双向队列管理等待线程,实现了线程的有序排队和高效调度,同时支持公平/非公平两种模式,兼顾性能与公平性。

3. AQS的核心设计思想

AQS的设计遵循"模板方法模式",核心思想可以概括为3点,也是AQS的三大核心组成部分:

  • 一个volatile状态变量(state):用于表示资源的占用状态,子类通过操作该状态实现资源的获取与释放;

  • 一个FIFO双向等待队列:用于存储获取资源失败的线程,线程进入队列后会阻塞,等待被唤醒后重新竞争资源;

  • 一套模板方法+钩子方法:AQS定义了获取/释放资源的核心流程(模板方法),将具体的资源操作逻辑(钩子方法)交给子类实现,无需子类关心排队和调度细节。

一句话总结:AQS做"通用的事"(排队、阻塞、唤醒),子类做"具体的事"(资源的获取与释放),分工明确,灵活高效。

二、AQS底层结构拆解:状态+队列,缺一不可

AQS的底层核心是"状态变量state"和"双向等待队列",两者协同工作,实现线程的同步与调度。下面我们逐一拆解这两个核心组件,结合源码片段,搞懂其底层实现。

1. 核心状态变量:volatile int state

(1)state的作用

state是AQS中最核心的成员变量,用volatile修饰,保证了线程之间的可见性,用于表示资源的占用状态,其具体含义由子类决定(AQS不固定state的含义)。常见的state含义:

  • ReentrantLock中:state表示"锁的重入次数",0表示锁未被占用,大于0表示锁被占用(state的值等于重入次数);

  • Semaphore中:state表示"可用资源的数量",0表示无可用资源,线程需排队等待;

  • CountDownLatch中:state表示"倒计时计数器",0表示倒计时结束,所有等待线程被唤醒。

(2)state的核心操作方法

AQS提供了3个核心方法操作state,均基于CAS实现,保证操作的原子性,避免线程安全问题,子类可直接调用:

  • getState():获取当前state的值,简单的读操作,因为state是volatile的,无需加锁;

  • setState(int newState):设置state的值,仅在子类确定当前线程拥有操作权限时使用(如释放锁时);

  • compareAndSetState(int expect, int update):CAS操作,仅当当前state的值等于expect时,将其更新为update,返回true;否则返回false,是实现线程安全竞争的核心方法。

示例源码(AQS中state相关定义):

java 复制代码
// 核心状态变量,volatile保证可见性
private volatile int state;

// 获取当前状态
protected final int getState() {
    return state;
}

// 设置状态(无CAS,需子类保证线程安全)
protected final void setState(int newState) {
    state = newState;
}

// CAS更新状态,原子操作
protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

注意:AQS本身不限制state的取值范围和含义,完全由子类根据自身需求定义,这也是AQS灵活性的核心体现。

2. 双向等待队列:CLH变体队列

当线程通过CAS获取state失败(即资源被其他线程占用)时,不会一直自旋重试,而是会被封装成一个"节点(Node)",加入到AQS的双向等待队列中,然后阻塞自己,等待被前驱节点唤醒------这个队列是AQS实现线程排队的核心,本质是一个CLH锁队列的变体(CLH是一种基于链表的无锁队列,用于实现公平锁)。

(1)队列的结构

AQS的等待队列是一个FIFO(先进先出)双向链表,每个节点代表一个等待中的线程,队列有两个核心指针:head(头节点)和tail(尾节点),初始时head和tail都为null,当有线程加入队列时,tail指针不断后移,head指针仅在有线程释放资源、唤醒后继线程时移动。

队列结构示意图(简化):

head(哨兵节点) ←→ Node1(线程1) ←→ Node2(线程2) ←→ ... ←→ tail(尾节点)

说明:head节点是一个"哨兵节点"(空节点),不对应任何等待线程,其作用是简化队列的操作(避免判断head是否为null),真正的等待线程从head的后继节点开始。

(2)Node节点的核心属性

每个Node节点封装了等待线程的相关信息,核心属性如下(源码简化版):

java 复制代码
static final class Node {
    // 标记节点的等待状态(核心属性)
    volatile int waitStatus;
    // 前驱节点
    volatile Node prev;
    // 后继节点
    volatile Node next;
    // 当前节点对应的线程
    volatile Thread thread;
    // 条件队列相关(后续讲解)
    Node nextWaiter;

    // 等待状态的常量值
    static final int CANCELLED =  1;  // 线程被取消(如超时、中断)
    static final int SIGNAL    = -1;  // 后继节点需要被唤醒
    static final int CONDITION = -2;  // 线程在条件队列中等待
    static final int PROPAGATE = -3;  // 共享模式下,状态需要传播
}

其中,waitStatus是Node节点的核心属性,用于标记当前线程的等待状态,决定了线程的行为(是否需要阻塞、是否能被唤醒),各状态的含义如下:

  • CANCELLED(1):线程因超时或被中断,已取消等待,该节点会被从队列中移除,不再参与资源竞争;

  • SIGNAL(-1):当前节点的后继节点正在阻塞,当前节点释放资源后,需要唤醒后继节点;

  • CONDITION(-2):线程在条件队列中等待(通过Condition.await()方法进入),需等待条件满足后被唤醒;

  • PROPAGATE(-3):仅用于共享模式,标识当前资源的释放状态需要向后传播,唤醒所有等待的线程;

  • 0:默认状态,线程正常等待,未被取消、未被标记为唤醒后继节点。

(3)队列的核心操作:入队与出队

AQS封装了队列的入队(enq)和出队(deq)操作,均基于CAS实现,保证线程安全,子类无需关心具体实现,核心流程如下:

  1. 入队操作(enq):线程获取资源失败后,调用enq方法将节点加入队列尾部。

    1. 若队列为空(head和tail都为null),先创建一个哨兵节点作为head,再将当前节点作为tail;

    2. 若队列不为空,通过CAS将当前节点设置为新的tail,同时将原tail的next指向当前节点;

    3. 入队完成后,线程调用LockSupport.park()方法阻塞自己,等待被唤醒。

  2. 出队操作(deq):线程释放资源后,会唤醒head的后继节点,该节点获取资源成功后,将自己设置为新的head(原head节点被回收),完成出队。

    1. 出队仅发生在"线程获取资源成功"时,由获取资源成功的线程主动完成;

    2. 出队后,新的head节点仍为哨兵节点,保证队列结构的一致性。

核心总结:队列的入队和出队操作均为线程安全,通过CAS避免并发问题,FIFO的结构保证了线程排队的有序性,哨兵节点的设计简化了队列操作。

三、AQS核心机制:模板方法+两种工作模式

AQS的核心竞争力在于"模板方法模式"的设计,以及支持"独占模式"和"共享模式"两种工作模式,适配不同的同步场景(如独占锁、共享锁)。下面我们详细讲解这两个核心机制,结合源码理解AQS的工作流程。

1. 模板方法模式:AQS的"骨架"

AQS定义了一套"获取资源"和"释放资源"的模板方法,这些方法是final修饰的,子类无法重写,保证了核心流程的一致性;同时提供了一组"钩子方法"(protected修饰,空实现或抛出异常),子类需要根据自身需求重写这些钩子方法,实现具体的资源获取/释放逻辑。

(1)核心模板方法(获取资源)

AQS提供了两种获取资源的模板方法,对应两种工作模式,核心逻辑一致:先尝试获取资源,获取失败则入队阻塞,等待被唤醒后重新尝试。

  • 独占模式获取资源acquire(int arg),对应ReentrantLock等独占锁,同一时刻只有一个线程能获取资源。
java 复制代码
// 独占模式获取资源,模板方法,final不可重写 
public final void acquire(int arg) { 
// 1. 尝试获取资源(钩子方法,子类实现) 
// 2. 若获取失败,将当前线程封装为Node,加入队列 
// 3. 阻塞当前线程,等待被唤醒后重新尝试获取 
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) { 
// 若线程被中断,补充中断响应 
selfInterrupt(); 
    } 
}
  • 共享模式获取资源acquireShared(int arg),对应Semaphore、CountDownLatch等共享锁,同一时刻多个线程可获取资源。
java 复制代码
// 共享模式获取资源,模板方法,final不可重写 
public final void acquireShared(int arg) { 
// 1. 尝试获取共享资源(钩子方法,子类实现) 
// 2. 若获取失败,入队阻塞 
if (tryAcquireShared(arg) < 0) { 
doAcquireShared(arg); 
    } 
}
(2)核心模板方法(释放资源)

释放资源的模板方法同样分为独占模式和共享模式,核心逻辑是:释放资源(更新state),然后唤醒队列中的后继线程,让其重新竞争资源。

  • 独占模式释放资源
java 复制代码
release(int arg)public final boolean release(int arg) { 
// 1. 尝试释放资源(钩子方法,子类实现) 
if (tryRelease(arg)) { 
// 2. 唤醒head的后继节点 
Node h = head; 
if (h != null && h.waitStatus != 0) { 
                    unparkSuccessor(h); 
                } 
            return true; 
        } 
    return false; 
}
  • 共享模式释放资源
java 复制代码
releaseShared(int arg)public final boolean releaseShared(int arg) { 
// 1. 尝试释放共享资源(钩子方法,子类实现) 
if (tryReleaseShared(arg)) { 
// 2. 唤醒后继节点,共享模式可能需要唤醒多个线程 
doReleaseShared(); 
            return true; 
        } 
    return false; 
}
(3)钩子方法(子类需重写)

AQS的钩子方法均为protected修饰,子类根据自身需求重写,未重写则抛出UnsupportedOperationException异常,核心钩子方法如下:

钩子方法 模式 作用 示例子类
tryAcquire(int arg) 独占模式 尝试获取独占资源,返回true表示获取成功,false表示失败 ReentrantLock
tryRelease(int arg) 独占模式 尝试释放独占资源,返回true表示释放成功,false表示失败 ReentrantLock
tryAcquireShared(int arg) 共享模式 尝试获取共享资源,返回>=0表示获取成功,<0表示失败 Semaphore、CountDownLatch
tryReleaseShared(int arg) 共享模式 尝试释放共享资源,返回true表示释放成功,false表示失败 Semaphore、CountDownLatch
isHeldExclusively() 独占模式 判断当前线程是否独占资源,用于Condition相关操作 ReentrantLock

核心总结:模板方法定义了"获取-排队-阻塞-唤醒-释放"的完整流程,钩子方法定义了"资源如何获取/释放",子类只需重写钩子方法,就能快速实现同步工具------这就是AQS的强大之处。

2. 两种工作模式:独占模式 vs 共享模式

AQS支持两种资源竞争模式,分别对应不同的同步场景,子类可根据需求选择实现其中一种模式,也可同时实现两种模式(如ReentrantReadWriteLock,读锁是共享模式,写锁是独占模式)。

(1)独占模式(Exclusive Mode)

核心特点:同一时刻,只有一个线程能获取资源,其他线程只能排队等待,直到持有资源的线程释放资源。

  • 适用场景:独占锁(如ReentrantLock)、互斥同步(确保同一时刻只有一个线程执行临界区代码);

  • 核心逻辑:线程获取资源时,需判断state是否为0(未被占用),若为0则通过CAS将state设为1(或重入次数),获取成功;若不为0,则入队阻塞;释放资源时,将state重置为0,唤醒后继线程。

  • 补充:独占模式支持"重入"(如ReentrantLock),即持有资源的线程可再次获取资源,只需将state自增(重入次数+1),释放时自减,直到state为0时真正释放资源。

(2)共享模式(Shared Mode)

核心特点:同一时刻,多个线程可同时获取资源,资源的可用数量会随着线程的获取而减少,随着线程的释放而增加。

  • 适用场景:共享锁(如Semaphore的许可、CountDownLatch的倒计时、ReentrantReadWriteLock的读锁);

  • 核心逻辑:线程获取资源时,判断state是否大于0(有可用资源),若大于0则通过CAS减少state(可用资源-1),获取成功;若等于0,则入队阻塞;释放资源时,通过CAS增加state(可用资源+1),唤醒所有等待的线程(因为多个线程可同时获取资源)。

  • 补充:共享模式下,线程获取资源后,不会阻止其他线程获取资源(只要有可用资源),适合"读多写少"的场景(如缓存读取)。

(3)两种模式对比
对比维度 独占模式 共享模式
资源占用 同一时刻仅一个线程占用 同一时刻多个线程占用
state含义 锁的重入次数(0表示未占用) 可用资源数量(0表示无可用)
唤醒逻辑 释放资源时,仅唤醒一个后继线程 释放资源时,唤醒所有等待线程
代表子类 ReentrantLock(写锁) Semaphore、CountDownLatch、ReentrantReadWriteLock(读锁)

四、实战解析:AQS在ReentrantLock中的应用

光懂理论不够,我们结合ReentrantLock(可重入独占锁)的源码,看看AQS的钩子方法是如何被重写的,以及AQS的核心流程如何工作------ReentrantLock是AQS独占模式的典型实现,也是我们日常开发中最常用的同步工具之一。

1. ReentrantLock的核心实现

ReentrantLock内部有一个静态内部类Sync,该类继承自AQS,重写了AQS的tryAcquire、tryRelease、isHeldExclusively三个钩子方法,实现了独占模式的资源获取与释放。ReentrantLock支持公平锁和非公平锁,两者的区别仅在于tryAcquire方法的实现不同。

(1)非公平锁的tryAcquire实现(默认)

非公平锁的特点:线程获取资源时,不会先判断队列是否有等待线程,而是直接尝试CAS获取资源,若获取成功则直接持有锁,效率更高(但可能导致线程饥饿)。

java 复制代码
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    // 重写AQS的tryAcquire方法,尝试获取独占资源
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        // 1. 若state为0(锁未被占用),直接CAS获取锁
        if (c == 0) {
            if (compareAndSetState(0, acquires)) {
                // 标记当前线程为锁的持有者
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 2. 若当前线程已持有锁(重入),state自增
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0) // 溢出判断
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        // 3. 锁被其他线程持有,获取失败
        return false;
    }
}
(2)tryRelease方法的实现

释放锁时,减少state(重入次数),当state减为0时,标记锁的持有者为null,真正释放锁。

java 复制代码
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    // 只有锁的持有者才能释放锁
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 当state减为0时,释放锁,标记持有者为null
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

2. ReentrantLock的工作流程(结合AQS)

以非公平锁为例,ReentrantLock的lock()和unlock()方法的工作流程,本质就是AQS模板方法的执行流程:

  1. 线程调用lock()方法,底层调用AQS的acquire(1)模板方法;

  2. acquire(1)调用ReentrantLock重写的tryAcquire(1)方法,尝试获取资源:

    1. 若state为0,CAS将state设为1,标记当前线程为持有者,获取成功,流程结束;

    2. 若当前线程已持有锁(重入),state自增1,获取成功,流程结束;

    3. 若锁被其他线程持有,tryAcquire返回false,进入下一步。

  3. AQS将当前线程封装为Node节点,加入双向等待队列,调用LockSupport.park()阻塞线程;

  4. 持有锁的线程调用unlock()方法,底层调用AQS的release(1)模板方法;

  5. release(1)调用ReentrantLock重写的tryRelease(1)方法,state减1:

    1. 若state减为0,释放锁(标记持有者为null),返回true;

    2. AQS唤醒head的后继节点,被唤醒的线程重新尝试获取资源(重复步骤2)。

五、AQS进阶:条件变量(Condition)

除了核心的状态管理和队列调度,AQS还提供了条件变量(Condition)的支持,用于实现"线程等待-唤醒"的精细化控制------Condition类似于Object的wait()和notify()方法,但比其更灵活(支持多个条件队列)。

1. Condition的核心作用

Condition依托于AQS实现,每个Condition对应一个条件队列(单向链表),用于存储调用Condition.await()方法后阻塞的线程。当条件满足时,调用Condition.signal()或signalAll()方法,唤醒条件队列中的线程,让其重新竞争资源。

核心优势:一个锁可以对应多个Condition,实现不同条件下的线程等待与唤醒,例如:生产者-消费者模型中,可通过两个Condition分别管理"生产者等待队列"和"消费者等待队列",避免不必要的唤醒。

2. Condition的核心方法

  • await():线程调用该方法后,释放所持有的锁,进入当前Condition的条件队列,阻塞自己,等待被唤醒;

  • signal():唤醒条件队列中的第一个线程,将其转移到AQS的等待队列中,让其重新竞争锁;

  • signalAll():唤醒条件队列中的所有线程,将其全部转移到AQS的等待队列中。

3. Condition与AQS队列的关系

AQS有两个核心队列:同步队列 (双向链表,用于存储获取锁失败的线程)和条件队列(单向链表,用于存储Condition.await()阻塞的线程),两者的关系如下:

  • 线程调用Condition.await()时,会先释放锁,然后从同步队列中移除,加入到条件队列中,阻塞自己;

  • 线程调用Condition.signal()时,会将条件队列中的线程移出,加入到同步队列中,等待重新获取锁;

  • 条件队列是依赖于同步队列存在的,只有持有锁的线程,才能调用Condition的await()和signal()方法(否则会抛出IllegalMonitorStateException异常)。

六、常见误区与注意事项

学习AQS时,很容易陷入一些误区,这里总结几个高频误区,帮助大家避坑:

  • 误区1:AQS是一个锁------错误。AQS不是锁,而是一个同步器框架,是实现锁和同步工具的"底层骨架",ReentrantLock、Semaphore等才是锁/同步工具。

  • 误区2:state的值只能是0或1------错误。state的含义由子类定义,可根据需求取任意整数(如ReentrantLock中state是重入次数,可大于1;Semaphore中state是可用资源数,可大于1)。

  • 误区3:AQS的队列是单向链表------错误。AQS的等待队列是双向链表,双向链表的优势是便于节点的删除(如线程被取消时,可快速调整前驱和后继节点的指针)。

  • 误区4:共享模式下,所有线程都能获取资源------错误。共享模式下,线程能否获取资源,取决于state的值(可用资源数),当state为0时,线程仍需排队等待。

  • 注意事项:子类重写钩子方法时,必须保证线程安全(通常基于CAS操作);Condition的await()和signal()方法,必须在持有锁的情况下调用,否则会抛出异常。

七、总结:AQS的核心价值与应用场景

AQS作为Java并发包的核心骨架,其核心价值在于"封装通用同步逻辑,简化同步工具的开发"------它将线程排队、阻塞唤醒、状态管理等复杂逻辑封装起来,让开发者只需关注"资源的获取与释放",就能快速实现安全高效的同步工具。

AQS的核心本质可以概括为:一个volatile状态变量(state)+ 一个FIFO双向等待队列 + 一套模板方法 + 两种工作模式,四种组件协同工作,实现了线程的有序竞争与高效调度。

常见的基于AQS实现的同步工具:

  • 独占模式:ReentrantLock(可重入独占锁)、ReentrantLock.WriteLock(写锁);

  • 共享模式:Semaphore(信号量)、CountDownLatch(倒计时器)、CyclicBarrier(循环屏障)、ReentrantLock.ReadLock(读锁)。

掌握AQS,不仅能让你更好地理解Java并发工具的底层原理,在遇到并发问题时,还能基于AQS自定义同步工具,满足特定业务场景的需求------这也是Java并发编程从"会用"到"精通"的关键一步。

相关推荐
愤豆2 小时前
08-Java语言核心-JVM原理-垃圾收集详解
java·开发语言·jvm
逸Y 仙X2 小时前
文章十四:ElasticSearch Reindex重建索引
java·大数据·数据库·elasticsearch·搜索引擎·全文检索
wregjru2 小时前
【读书笔记】Effective C++ 条款8:别让异常逃离析构函数
java·开发语言
harder3212 小时前
Swift 面向协议编程的 RMP 模式
开发语言·ios·mvc·swift·策略模式
烤麻辣烫2 小时前
I/O流 进阶流
java·开发语言·学习·intellij-idea
艾莉丝努力练剑2 小时前
【QT】QT快捷键整理
linux·运维·服务器·开发语言·图像处理·人工智能·qt
程序员_大白2 小时前
【2025版】最新Qt下载安装及配置教程(非常详细)零基础入门到精通,收藏这篇就够了
开发语言·qt
枫叶丹42 小时前
【HarmonyOS 6.0】ArkData 分布式数据对象新特性:资产传输进度监听与接续传输能力深度解析
开发语言·分布式·华为·wpf·harmonyos
冷血~多好2 小时前
mysql实现主从复制以及springboot实现读写分离
java·数据库·mysql·springboot