AQS

AQSAbstractQueuedSynchronizer的简称,即抽象的队列同步器

AQSAbstractQueuedSynchronizer的简称,即抽象的队列同步器

  • 抽象:抽象类,只实现一些主要逻辑,有些方法由子类实现;
  • 队列:使用先进先出(FIFO)的队列存储数据;
  • 同步:实现了同步的功能。

为什么需要 AQS

AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的同步器 简单的说, 存储并管理线程同步的模板

具体的同步器(如 ReentrantLockCountDownLatchSemaphore, FutureTask等)是通过继承 AQS 并实现一些抽象方法来实现资源的具体获取和释放行为的。

AQS的作用是什么?

了解 自旋锁与CLH锁

AQS 是一个 抽象类,它为具体的同步器提供了一个通用的执行框架。 它定义了如何获取和释放共享资源的基本流程,但并没有实现具体的逻辑。

AQS 提供了同步器所需要的 框架和基础设施,比如:

  1. 如何在多个线程间协调资源的竞争。
  2. 如何管理线程的队列(阻塞队列)以等待资源。

其中AQS 的核心数据结构是基于CLH 锁改进的, 详情下述

  1. 线程的挂起与唤醒等机制。

AQS的数据结构

既然是控制线程, 那么数据结构应对并发相应的优化 AQS的核心数据结构是基于 CLH队列锁改进的 Pasted image 20250203211340.png CLH队列锁有如下缺点

  1. 仍然基于自旋, 长时间自旋下CPU占用高
  2. 功能单一, 不能挂起, 不能共享读, 只支持独占等 针对以上缺点, AQS进行改造
  3. AQS 将自旋操作改为阻塞线程操作。
  4. AQS 对 CLH 锁进行改造和扩展, 扩展每个节点的状态、显式的维护前驱节点和后继节点

AQS 内部使用了一个volatile关键字 的变量 state 来作为线程的状态标识。

volatile int waitStatus;

该变量有如下状态, AQS提供原子读写 ![[content/en/java/Basic/Concurrent/Pasted image 20250203210552.png]]

  • CANCELLED:表示当前节点(对应的线程)已被取消。当等待超时或被中断,会触发进入为此状态,进入该状态后节点状态不再变化;
  • SIGNAL:后面节点等待当前节点唤醒;
  • CONDITION:当前线程阻塞在Condition,如果其他线程调用了Condition的signal方法,这个节点将从等待队列转移到同步队列队尾,等待获取同步锁;

了解Condition等待通知条件

  • PROPAGATE:共享模式,前置节点唤醒后面节点后,唤醒操作无条件传播下去;
  • 0:中间状态,当前节点后面的节点已经唤醒,但是当前节点线程还没有执行完成。 状态多了不少, 但是AQS为了优化, 抛弃了自旋设计,这导致线程被阻塞时没办法获取前驱节点的状态 所以AQS显式维护前后节点, 在锁释放时主动通知后继线程解除阻塞 Pasted image 20250203211000.png

(T1 释放锁后主动唤醒 T2,使 T2 检测到锁已释放,获取锁成功。)

在释放锁时,如果当前节点的后驱节点不可用时,将从利用队尾指针 Tail 从尾部遍历到直到找到当前节点正确的后驱节点。 这是因为双端队列的插入没有相应的原子操作, 因此后驱节点的设置并非作为原子性插入操作的一部分,而仅是在节点被插入后简单地赋值

AQS还实现了共享读 资源有两种共享模式,或者说两种同步方式:

  • 独占模式(Exclusive):资源是独占的,一次只能有一个线程获取。如 ReentrantLock重入锁 _index

  • 共享模式(Share):同时可以被多个线程获取,具体的资源个数可以通过参数指定。如 Semaphore/CountDownLatch

这两种模式是在内部类 Node 中实现的

Node源码

可以看到 node中的状态信息都是常量, 无法修改的

static final class Node {
    // 标记一个结点(对应的线程)在共享模式下等待
    static final Node SHARED = new Node();
    // 标记一个结点(对应的线程)在独占模式下等待
    static final Node EXCLUSIVE = null;

    // waitStatus的值,表示该结点(对应的线程)已被取消
    static final int CANCELLED = 1;
    // waitStatus的值,表示后继结点(对应的线程)需要被唤醒
    static final int SIGNAL = -1;
    // waitStatus的值,表示该结点(对应的线程)在等待某一条件
    static final int CONDITION = -2;
    /*waitStatus的值,表示有资源可用,新head结点需要继续唤醒后继结点(共享模式下,多线程并发释放资源,而head唤醒其后继结点后,需要把多出来的资源留给后面的结点;设置新的head结点时,会继续唤醒其后继结点)*/
    static final int PROPAGATE = -3;

    // 等待状态,取值范围,-3,-2,-1,0,1
    volatile int waitStatus;
    volatile Node prev; // 前驱结点
    volatile Node next; // 后继结点
    volatile Thread thread; // 结点对应的线程
    Node nextWaiter; // 等待队列里下一个等待条件的结点


    // 判断共享模式的方法
    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    Node(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }

    // 其它方法忽略,可以参考具体的源码
}

// AQS里面的addWaiter私有方法
private Node addWaiter(Node mode) {
    // 使用了Node的这个构造函数
    Node node = new Node(Thread.currentThread(), mode);
    // 其它代码省略
}

AQS源码解析

AQS 的设计是基于模板方法的,它有一些方法必须要子类实现,它们主要有:

  • isHeldExclusively():该线程是否正在独占资源。只有用到 condition 才需要去实现它。
  • tryAcquire(int):独占方式。尝试获取资源,成功则返回 true,失败则返回 false。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回 true,失败则返回 false。
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0 表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回 true,否则返回 false。

比较特殊的是, 这里的抽象方法并没有加 abstract 关键字 这是因为并非AQS的所有抽象方法都需要子类实现, 所以子类只重写需要的方法就行

子类不是重点, 我们来看模板类, 也就是AQS的逻辑

获取资源

获取资源的入口是 acquire(int arg)方法。arg 是要获取的资源个数,在独占模式下始终为 1。我们先来看看这个方法的逻辑:

public final void accquire(int arg) {
    // tryAcquire 再次尝试获取锁资源,如果尝试成功,返回true,尝试失败返回false
    if (!tryAcquire(arg) &&
        // 走到这,代表获取锁资源失败,需要将当前线程封装成一个Node,追加到AQS的队列中
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        // 线程中断
        selfInterrupt();
}

首先调用 tryAcquire 尝试去获取资源。如果获取资源失败,就通过 addWaiter(Node.EXCLUSIVE) 方法把这个线程插入到等待队列中。其中传入的参数代表要插入的 Node 是独占式的。

private Node addWaiter(Node mode) {
 //创建 Node 类,并且设置 thread 为当前线程,设置为排它锁
 Node node = new Node(Thread.currentThread(), mode);
 // 获取 AQS 中队列的尾部节点
 Node pred = tail;
 // 如果 tail == null,说明是空队列,
 // 不为 null,说明现在队列中有数据,
 if (pred != null) {
  // 将当前节点的 prev 指向刚才的尾部节点,那么当前节点应该设置为尾部节点
  node.prev = pred;
  // CAS 将 tail 节点设置为当前节点
  if (compareAndSetTail(pred, node)) {
   // 将之前尾节点的 next 设置为当前节点
   pred.next = node;
   // 返回当前节点
   return node;
  }
 }
 enq(node);
 return node;
}

// 自旋CAS插入等待队列
private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

在队列的尾部插入新的 Node 节点,但是需要注意的是由于 AQS 中会存在多个线程同时争夺资源的情况,因此肯定会出现多个线程同时插入节点的操作,在这里是通过 CAS 自旋的方式保证了操作的线程安全性。

释放资源
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) {
    // 如果状态是负数,尝试把它设置为0
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);
    // 得到头结点的后继结点head.next
    Node s = node.next;
    // 如果这个后继结点为空或者状态大于0
    // 通过前面的定义我们知道,大于0只有一种可能,就是这个结点已被取消(只有 Node.CANCELLED(=1) 这一种状态大于0)
    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);
}

如果tryRelease(arg)成功释放了锁,那么接下来会检查队列的头结点。如果头结点存在并且waitStatus不为0(这意味着有线程在等待),那么会调用unparkSuccessor(Node h)方法来唤醒等待的线程。

Last modified March 8, 2025: interview (3799c36)