AQS:AbstractQueuedSynchronizer

0 阅读8分钟

什么是AQS?

AQS(AbstractQueuedSynchronizer)是Java并发包(java.util.concurrent.locks)中的一个核心框架,用于构建ReentrantLock)和其他同步器(如SemaphoreCountDownLatch)的基础。它封装了一个同步器时最复杂、最易错的部分:线程阻塞队列的管理、线程的阻塞与唤醒

只需要定义“资源能否被获取”的规则,而“线程排队等待”的繁琐工作由AQS自动完成。

同步状态管理

AQS内部维护了一个volatile int state变量,代表共享资源的状态。

同步状态的语义由子类定义。例如,在ReentrantLock中,state=0表示锁空闲,state>0表示锁被持有,且数值表示重入次数;在Semaphore中,state表示可用许可证数量。

相关的api

protected final int getState(): 获取当前状态值。
protected final void setState(int newState): 设置状态值。
protected final boolean compareAndSetState(int expect, int update): 核心,通过CAS原子操作更新状态,保证并发安全。

需要子类实现的关键方法:

protected boolean tryAcquire(int arg): 尝试以独占模式获取资源。成功返回true,失败返回false。arg是获取资源的数量,通常为1protected boolean tryRelease(int arg): 尝试释放独占模式的资源。成功返回trueprotected int tryAcquireShared(int arg): 尝试以共享模式获取资源。返回负值表示失败;0表示成功,但无剩余资源;正值表示成功,且有剩余资源。
protected boolean tryReleaseShared(int arg): 尝试释放共享模式的资源。

供外部调用的模板方法:

子类实现上述tryXXX方法后,外部就可以调用AQS提供的以下模板方法,这些方法内部会调用你实现的tryXXX方法,并负责复杂的队列管理。

public final void acquire(int arg): 获取资源的主入口。如果tryAcquire成功则直接返回,否则线程会进入等待队列,可能被反复阻塞和唤醒,直到成功。不可中断。
public final boolean release(int arg): 释放资源。调用tryRelease成功后,会唤醒队列中一个等待线程。
public final void acquireShared(int arg)
public final boolean releaseShared(int arg)

一个FIFO线程等待队列

CLH队列是一种经典的、用于实现高效自旋锁同步队列数据结构。其核心设计目标是在共享内存多处理器系统上,减少锁竞争带来的总线流量缓存一致性开销,从而实现高性能的锁机制。

可以将它理解为一个隐式的、虚拟的“排队队列”,其核心思想是:每个试图获取锁的线程,只监听(自旋检查)其前驱线程的状态,而不是全局的锁状态。

  1. 队列节点: 每个竞争锁的线程都会持有一个自己的节点(QNode)。节点中最关键的字段是一个布尔值 locked(或 isLocked)。

    • locked = true: 表示该线程正在持有锁,或者正在等待锁(即它还没有获取到锁,但已经在队列中)。
    • locked = false: 表示该线程已经释放了锁,后继线程可以获取锁了。
  2. 队列结构

    • 队列有一个“尾巴”指针(tail),这是一个可以被所有线程访问的原子变量
    • 当一个新线程尝试获取锁时,它会创建一个新节点,并通过原子操作(如getAndSetcompareAndSwap)将自己设置为新的 tail,同时获取到旧的 tail(即它的前驱节点)。
    • 线程们通过这个“获取前驱”的操作,隐式地形成了一个链表式的队列,但节点之间并没有真正的next指针连接,而是通过操作tail的顺序来逻辑链接。

自旋等待

  • 线程在成功将自己加入队列尾部后,不会去轮询一个全局的锁标志,而是持续地自旋检查其前驱节点的 locked字段
  • 只要前驱节点的 lockedtrue,它就继续等待(自旋)。
  • 一旦检测到前驱节点的 locked变为 false,就意味着前驱线程已经释放了锁,自己可以获取锁并开始执行临界区代码。

释放锁

  • 线程执行完临界区代码后,将自己节点的 locked设置为 false
  • 这样,正在自旋监听它的后继线程就会立即发现这个变化,并结束等待,成功获取锁。

对比CAS锁的优势:

  1. 减少缓存一致性流量核心优势):

    • 在传统自旋锁中,所有等待线程都在自旋读取同一个内存位置(锁标志)。这会导致严重的“缓存一致性”问题,每次锁状态变化都会使所有CPU核心的缓存失效,产生巨大的总线流量,严重影响性能。
    • 在CLH锁中,每个线程自旋检查的是自己前驱节点(一个局部变量或独立内存块)的状态。这个状态通常位于线程的本地缓存或邻近核心的缓存中,通信开销极小。锁释放时,只需要修改一个变量来通知一个后继线程,极大地减少了全局内存争用。
  2. 保证公平性

    • CLH锁是一个严格的先来先服务(FIFO) 队列,绝对公平。这可以防止线程“饿死”。

AQS中的FIFO队列就是对CLH队列的一种变体。

工作模式

AQS支持两种资源访问模式:

  1. 独占模式:资源一次只能被一个线程持有。
  2. 共享模式:资源可以同时被多个线程持有。

两种模式的线程在同一个队列中排队,但被唤醒和获取成功的逻辑不同。

AQS的使用

AQS使用了典型的模板方法模式,作为使用者,只需要关心状态 state的含义和变更规则。具体方式是重写以下几个保护方法

方法名描述需要重写场景
tryAcquire(int)独占模式。尝试获取资源,成功返回true,失败返回false。实现独占锁(如Mutex
tryRelease(int)独占模式。尝试释放资源,成功返回true,失败返回false。实现独占锁
tryAcquireShared(int)共享模式。尝试获取资源。返回负数表示失败;0表示成功,但无剩余资源;正数表示成功,且有余量。实现信号量、闭锁
tryReleaseShared(int)共享模式。尝试释放资源,如果释放后允许唤醒后续等待线程则返回true。实现信号量、闭锁
isHeldExclusively()当前线程是否独占资源。用于Condition的实现。实现条件变量时

AQS的顶级流程:

  • acquire(int arg)方法(已由AQS实现,不可重写) :

    1. 调用子类的 tryAcquire(arg)尝试直接获取。
    2. 如果失败,则将当前线程加入等待队列,并可能将其挂起(LockSupport.park)。
    3. 线程被唤醒后,会再次尝试 tryAcquire
  • release(int arg)方法:

    1. 调用子类的 tryRelease(arg)尝试释放。
    2. 如果释放成功,则唤醒队列中第一个等待的线程。

公平和非公平 这是AQS的一个精妙设计。默认行为是非公平的

  • 非公平:一个新来的线程(Thread A)在 acquire时,会先直接调用 tryAcquire尝试“插队”,如果成功,即使队列里有等待的线程,Thread A也能立即获取锁。这提升了吞吐量,但可能导致“饥饿”。
  • 公平:在重写的 tryAcquire方法中,先检查队列中是否有比自己更早等待的线程(通过 hasQueuedPredecessors()方法)。如果有,则主动获取失败,乖乖去排队。这保证了严格的FIFO顺序。

Condition支持

AQS内部类 ConditionObject实现了 Condition接口,用于实现更灵活的线程等待/通知机制(类似于 Object.wait()/notify(),但更强大)。

  • 一个锁可以创建多个 Condition对象。
  • 只能在独占模式下使用。
  • await()会将当前线程放入该条件队列并释放锁;signal()会将条件队列中的一个线程转移到AQS主阻塞队列中,等待重新获取锁。

基于AQS实现的非公平不可重入锁

/**
     * 基于AQS实现的一个不可重入锁
     */
    static class NoReentrantLock implements Lock{

        class Sync extends AbstractQueuedSynchronizer{
            /**
             * 尝试获取锁
             * @param arg 获取的数量,固定为1
             *
             * @return 锁是否获取成功
             */
            @Override
            protected boolean tryAcquire(int arg) {
                if(arg!=1){
                    throw new IllegalArgumentException("arg must be 1")
                }
                // 通过AQS提供的cas设置State
                if(compareAndSetState(0,1)){
                    //成功获取锁,并将当前线程设置为独占线程
                    setExclusiveOwnerThread(Thread.currentThread());
                    return true;
                }
                return false;
            }

            /**
             * 尝试释放锁
             * @param arg 为释放数量,固定为1
             * @return
             */
            @Override
            protected boolean tryRelease(int arg) {
                if(arg!=1){
                    throw new IllegalArgumentException("arg must be 1");
                }
                if(getExclusiveOwnerThread()!=Thread.currentThread()){
                    throw new IllegalMonitorStateException("Current thread is not the lock holder");
                }
                // 清空锁持有线程
                setExclusiveOwnerThread(null);
                // 设置状态为0,因为state被volatile标记,因此后执行,防止指令重排序
                setState(0);
                return true;
            }

            /**
             * 判断是否被独占
             * @return
             */
            @Override
            protected boolean isHeldExclusively() {
                return getState()==1;
            }

            /**
             * 创建条件变量
             * @return
             */
            public Condition newCondition(){
                return new ConditionObject();
            }
        }
        private Sync sync=new Sync();

        /**
         * 获取锁
         */
        @Override
        public void lock() {
            sync.acquire(1);
        }

        /**
         *
         * @throws InterruptedException
         */
        @Override
        public void lockInterruptibly() throws InterruptedException {
            sync.acquireInterruptibly(1);
        }

        /**
         * 尝试获取锁,立即返回结果
         * @return
         */
        @Override
        public boolean tryLock() {
            return sync.tryAcquire(1);
        }
        /**
         * 尝试获取锁,带超时
         * @param time 超时时间
         * @param unit 时间单位
         * @return 是否获取成功
         * @throws InterruptedException 如果线程被中断
         */
        @Override
        public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
            return sync.tryAcquireNanos(1, unit.toNanos(time));
        }

        /**
         * 释放锁
         */
        @Override
        public void unlock() {
            sync.release(1);
        }

        @Override
        public Condition newCondition() {
            return sync.newCondition();
        }
    }

从上述的代码可以看出,AQS就是一个实现大部分代码的基础实现。

通过重写AQStryAcquiretryRelease来实现LocktryLockLock等方法。