一文带你读懂AQS

什么是AQS?

一、AQS基本介绍

1.1 什么是AQS

AbstractQueuedSynchronizer(AQS) 是Java并发编程的基础框架,位于java.util.concurrent.locks包下,由并发大师Doug Lea设计。它通过原子整数状态(state)FIFO等待队列实现了一套通用的线程同步机制,为ReentrantLock、Semaphore、CountDownLatch等同步工具提供统一的底层支持。

AQS的核心思想是模板方法模式:将复杂的线程排队、阻塞/唤醒等通用逻辑封装在抽象类中,而将同步状态的获取/释放等具体逻辑延迟到子类实现。开发者只需重写少量钩子方法(如tryAcquiretryRelease),即可快速构建自定义同步器。

1.2 核心组件

AQS的实现依赖于两个核心组件:

(1)同步状态(State)
  • private volatile int state变量表示,用于记录资源的占用情况。
  • 通过getState()setState()compareAndSetState()方法操作,确保线程安全。
  • 不同同步器对state的定义不同
    • ReentrantLock:state表示锁的重入次数(0=未持有,>0=持有)。
    • Semaphore:state表示剩余许可数量。
    • CountDownLatch:state表示倒计时计数器。
(2)CLH等待队列
  • 基于双向链表实现的FIFO队列,用于存放竞争资源失败的线程。
  • 每个节点(Node)包含线程引用、等待状态(waitStatus)、前驱/后继指针:
    static final class Node {
        volatile int waitStatus;  // 节点状态:CANCELLED(1)、SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3)
        volatile Node prev;        // 前驱节点
        volatile Node next;        // 后继节点
        volatile Thread thread;    // 关联线程
        Node nextWaiter;           // 条件队列或共享模式标记
    }
    
  • CLH队列的优势:将多线程对共享资源的竞争转化为对前驱节点状态的轮询,减少内存竞争(相比MCS锁更适合SMP架构)。

1.3 两种同步模式

AQS支持两种资源共享模式,可灵活适配不同场景:

模式特点典型实现
独占模式同一时刻仅一个线程可获取资源ReentrantLock、ReentrantReadWriteLock.WriteLock
共享模式多个线程可同时获取资源Semaphore、CountDownLatch、ReentrantReadWriteLock.ReadLock

二、AQS源码深度解析

2.1 核心数据结构

AQS的核心字段包括:

public abstract class AbstractQueuedSynchronizer {
    private transient volatile Node head;  // 队列头节点
    private transient volatile Node tail;  // 队列尾节点
    private volatile int state;            // 同步状态
}
  • head/tail:通过volatile修饰确保线程可见性,使用CAS操作原子更新。
  • Node节点状态
    • CANCELLED(1):线程因超时或中断取消等待。
    • SIGNAL(-1):后继节点需要被唤醒。
    • CONDITION(-2):节点在条件队列中等待。
    • PROPAGATE(-3):共享模式下释放操作需传播。

2.2 独占模式:acquire/release流程

以ReentrantLock为例,分析独占模式的核心逻辑:

(1)获取锁:acquire(int arg)
public final void acquire(int arg) {
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
        selfInterrupt();  // 中断当前线程
    }
}

流程拆解:

  1. tryAcquire:子类实现的尝试获取锁逻辑(如ReentrantLock的公平/非公平实现)。
  2. addWaiter:将当前线程封装为Node节点,通过CAS加入队列尾部。
  3. acquireQueued:节点进入队列后自旋尝试获取锁,失败则阻塞。

关键方法acquireQueued的自旋逻辑:

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;  // 帮助GC
                failed = false;
                return interrupted;
            }
            // 判断是否需要阻塞
            if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) {
                interrupted = true;
            }
        }
    } finally {
        if (failed) cancelAcquire(node);  // 取消获取
    }
}
(2)释放锁:release(int arg)
public final boolean release(int arg) {
    if (tryRelease(arg)) {  // 子类实现释放逻辑
        Node h = head;
        if (h != null && h.waitStatus != 0) {
            unparkSuccessor(h);  // 唤醒后继节点
        }
        return true;
    }
    return false;
}
  • unparkSuccessor:唤醒头节点的后继节点(通过LockSupport.unpark),确保公平性。

2.3 共享模式:acquireShared/releaseShared流程

以Semaphore为例,共享模式允许多个线程同时获取资源:

(1)获取共享资源:acquireShared(int arg)
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0) {  // 返回值<0表示获取失败
        doAcquireShared(arg);  // 进入队列等待
    }
}
  • tryAcquireShared:子类实现共享资源获取逻辑(如Semaphore判断剩余许可是否充足)。
(2)释放共享资源:releaseShared(int arg)
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {  // 子类实现释放逻辑
        doReleaseShared();  // 唤醒后续节点(可能传播唤醒)
        return true;
    }
    return false;
}
  • PROPAGATE状态:共享模式下,头节点释放资源后需唤醒所有等待线程(如CountDownLatch计数归0后唤醒所有等待线程)。

2.4 条件变量(ConditionObject)

AQS通过内部类ConditionObject实现条件等待/通知机制,类似synchronizedwait/notify,但支持多条件队列

public class ConditionObject implements Condition {
    private transient Node firstWaiter;  // 条件队列头
    private transient Node lastWaiter;   // 条件队列尾
}

核心方法

  • await():释放锁并进入条件队列,被唤醒后重新竞争锁。
  • signal():将条件队列头节点转移到同步队列,等待获取锁。

与Object.wait的对比

特性Object.waitCondition.await
锁依赖synchronizedLock
条件队列数量1个(与锁绑定)多个(可创建多个Condition)
唤醒方式随机唤醒或全部唤醒精确唤醒单个或全部节点

2.5 性能优化:自旋与阻塞

AQS通过自适应自旋减少线程阻塞开销:

  • 当线程获取锁失败时,先自旋几次(基于JVM动态调整次数),若仍失败则调用LockSupport.park阻塞。
  • 自旋的实现位于acquireQueuedshouldParkAfterFailedAcquire方法中,根据前驱节点状态判断是否需要阻塞。

三、AQS使用场景与自定义同步器

3.1 适用场景

AQS适用于实现各类同步工具,典型场景包括:

  • 互斥锁:如ReentrantLock,控制资源独占访问。
  • 限流器:如Semaphore,限制并发访问数量。
  • 倒计时器:如CountDownLatch,等待多个线程完成。
  • 读写分离锁:如ReentrantReadWriteLock,优化读多写少场景。

3.2 自定义同步器示例:简易互斥锁(Mutex)

通过继承AQS实现一个简单的互斥锁:

public class Mutex implements Lock {
    private final Sync sync = new Sync();

    // 内部类继承AQS
    private static class Sync extends AbstractQueuedSynchronizer {
        // 尝试获取独占锁(state=0时CAS设为1)
        @Override
        protected boolean tryAcquire(int arg) {
            return compareAndSetState(0, 1);
        }

        // 释放独占锁(state设为0)
        @Override
        protected boolean tryRelease(int arg) {
            setState(0);
            return true;
        }

        // 是否独占持有
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        // 创建条件变量
        Condition newCondition() {
            return new ConditionObject();
        }
    }

    // 实现Lock接口方法
    @Override public void lock() { sync.acquire(1); }
    @Override public void unlock() { sync.release(1); }
    @Override public Condition newCondition() { return sync.newCondition(); }
    // 其他方法(tryLock、lockInterruptibly等)省略...
}

使用示例

Mutex lock = new Mutex();
lock.lock();
try {
    // 临界区操作
} finally {
    lock.unlock();
}

四、JUC包中基于AQS的类

JUC包中多数同步工具均基于AQS实现,以下是核心类的分析:

4.1 ReentrantLock:可重入独占锁

  • 核心原理:state表示重入次数,tryAcquire中判断当前线程是否为持有者,支持公平/非公平模式。
  • 公平锁tryAcquire时检查队列是否有前驱节点,严格按FIFO顺序获取。
  • 非公平锁:直接CAS尝试获取锁,可能插队(默认模式,吞吐量更高)。

4.2 Semaphore:信号量

  • 核心原理:state表示剩余许可数,tryAcquireShared减少许可,tryReleaseShared增加许可。
  • 应用场景:限制并发线程数(如连接池资源控制)。

4.3 CountDownLatch:倒计时器

  • 核心原理:state初始化为计数N,countDown()递减state,await()在state=0时唤醒所有等待线程。
  • 特点:不可重置,一次性使用。

4.4 ReentrantReadWriteLock:读写锁

  • 核心原理:state按位拆分,高16位表示读锁计数,低16位表示写锁计数
  • 读锁:共享模式,tryAcquireShared检查写锁是否释放。
  • 写锁:独占模式,tryAcquire检查读锁和写锁是否释放。
  • 锁降级:支持写锁→读锁(获取写锁后获取读锁,再释放写锁),但不支持升级。

4.5 CyclicBarrier:循环屏障

  • 实现方式:基于ReentrantLock和Condition,间接使用AQS。
  • 特点:计数可重置,适用于循环执行的任务阶段同步。

五、总结

5.1 AQS的优势

  1. 通用性:统一实现线程排队、阻塞/唤醒逻辑,简化同步器开发。
  2. 灵活性:支持独占/共享两种模式,可按需组合(如ReadWriteLock)。
  3. 高性能:CLH队列减少内存竞争,自适应自旋降低阻塞开销。

5.2 局限性

  1. 复杂度:直接使用AQS需理解底层原理,门槛较高。
  2. 单一状态:仅通过一个int state变量表示状态,复杂场景需按位拆分(如读写锁)。

5.3 实践建议

  • 优先使用JUC工具:如ReentrantLock、Semaphore等,避免重复造轮子。
  • 自定义同步器:仅在现有工具无法满足需求时(如特殊资源控制逻辑),基于AQS实现。
  • 注意内存可见性:操作共享变量时需配合volatile或锁机制。

AQS作为Java并发编程的基石,其设计思想(如模板方法、CLH队列)和实现细节(如CAS操作、状态管理)对理解并发原理至关重要。掌握AQS不仅能帮助开发者更好地使用JUC工具,还能为自定义同步组件提供理论支撑。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值