Java中的AQS(AbstractQueuedSynchronizer)详解
字数 2822 2025-11-03 18:01:32

Java中的AQS(AbstractQueuedSynchronizer)详解

AQS是Java并发包java.util.concurrent.locks的核心框架,它为构建锁和同步器提供了一种底层的、高性能的、可扩展的基础。像ReentrantLock、Semaphore、CountDownLatch等常用的同步工具都是基于AQS构建的。

1. AQS的核心思想

AQS的核心思想非常简单:它维护了一个volatile int类型的同步状态(state)和一个FIFO(先进先出)的线程等待队列(CLH队列的变体)

  • 同步状态(state):这是一个可以被并发访问的变量,它的具体含义由子类定义。例如:
    • 在ReentrantLock中,state表示锁被持有的次数(0表示未锁定,1表示被一个线程锁定,>1表示被同一个线程重入)。
    • 在Semaphore中,state表示当前可用的许可证数量。
    • 在CountDownLatch中,state表示还需要等待的计数。
  • 等待队列:这是一个双向链表,用于存放所有等待获取资源的线程。当线程尝试获取资源失败时,它会被封装成一个节点(Node)并加入队列尾部,进入等待状态。当持有资源的线程释放资源时,它会唤醒队列中的下一个(或所有)等待线程。

2. AQS的模板方法模式

AQS使用了模板方法模式。它定义了一系列获取和释放资源的“骨架”方法(如acquirerelease),而将一些关键步骤的具体实现留给子类。子类需要重写以下几个protected方法来实现特定的同步语义:

  • tryAcquire(int arg):尝试以独占模式获取资源。成功返回true,失败返回false。
  • tryRelease(int arg):尝试以独占模式释放资源。成功返回true,失败返回false。
  • tryAcquireShared(int arg):尝试以共享模式获取资源。返回负数表示失败;0表示成功,但后续共享模式获取可能失败;正数表示成功,且后续共享模式获取可能成功。
  • tryReleaseShared(int arg):尝试以共享模式释放资源。
  • isHeldExclusively():当前同步器是否在独占模式下被线程占用。

3. 深入AQS的工作流程:以独占模式为例

我们以最典型的独占模式(如ReentrantLock)为例,详细拆解AQS的工作流程。

场景:线程A、B、C竞争一把基于AQS实现的锁。

步骤一:线程A尝试获取锁(lock.lock() -> acquire(1))

  1. 调用tryAcquire(1):AQS首先会调用子类重写的tryAcquire方法。假设此时锁是自由的(state为0)。
  2. 成功获取:在tryAcquire方法中,子类会通过CAS操作尝试将state从0改为1。线程A的CAS操作成功,state变为1,并且AQS会记录当前持有锁的线程是线程A(exclusiveOwnerThread = threadA)。
  3. 流程结束tryAcquire返回true,线程A成功获取锁,继续执行其临界区代码。整个过程非常快速,没有涉及队列操作。

步骤二:线程B尝试获取锁(此时锁已被线程A持有)

  1. 调用tryAcquire(1):同样,AQS先调用tryAcquire。此时state为1,且持有线程不是B,所以tryAcquire方法返回false,获取失败。
  2. 加入等待队列:由于tryAcquire失败,AQS会执行addWaiter(Node.EXCLUSIVE)方法。
    • 将线程B封装成一个Node节点(模式为EXCLUSIVE)。
    • 如果队列已经存在(尾节点tail不为null),则通过CAS操作快速地将新节点设置为尾节点。
    • 如果队列不存在(如这是第一个等待线程)或CAS失败,则进入一个“自旋”循环,直到通过CAS操作成功将新节点添加到队列尾部。
  3. 在队列中自旋等待:节点入队后,会调用acquireQueued方法。这是一个核心的自旋过程:
    • 检查当前节点的前驱节点(prev)是不是头节点(head)。如果是,说明自己是队列中的第一个等待者,有资格再次尝试获取锁(调用tryAcquire)。
    • 此时线程A尚未释放锁,所以tryAcquire依然失败。
    • 获取失败后,线程B会检查是否需要被挂起(park)。它会将前驱节点的waitStatus(等待状态)设置为SIGNAL,表示“当你释放锁时,需要唤醒我”。
    • 最后,调用LockSupport.park(this)将线程B挂起(进入WAITING状态),等待被唤醒。

步骤三:线程C也尝试获取锁

流程与线程B完全相同。线程C会被封装成Node,加入到等待队列中线程B的后面,然后被挂起。此时队列结构为:head -> [Node-B] -> [Node-C](head是一个虚拟节点)。

步骤四:线程A释放锁(lock.unlock() -> release(1))

  1. 调用tryRelease(1):AQS调用子类重写的tryRelease方法。该方法会将state减1。如果state变为0,表示锁已完全释放。
  2. 唤醒后继节点:如果tryRelease返回true(释放成功),AQS会检查队列中是否有等待的线程(即头节点的waitStatus不为0)。
  3. 找到需要唤醒的节点:AQS找到头节点的下一个节点(即线程B所在的节点)。
  4. 执行唤醒unparkSuccessor:调用LockSupport.unpark(node.thread)来唤醒线程B。

步骤五:线程B被唤醒后继续竞争

  1. 线程B从之前被挂起的地方(LockSupport.park)继续执行。
  2. 它再次进入acquireQueued的自旋循环。
  3. 这次,它发现自己的前驱节点是头节点,于是再次调用tryAcquire(1)
  4. 此时锁是自由的,线程B的CAS操作成功,将state从0改为1,并记录自己为锁的持有者。
  5. 线程B将自己设置为新的头节点(将原头节点出队)。
  6. 线程B成功获取锁,开始执行其临界区代码。

总结

AQS通过一个state变量和一個FIFO队列,精巧地管理了线程对共享资源的访问。其核心流程可以概括为:

  • 获取资源:先尝试tryAcquire,成功则直接返回。失败则将自己加入队列尾部,并自旋/等待,直到被前驱节点唤醒并成功获取资源。
  • 释放资源:执行tryRelease,成功后唤醒队列中下一个有效的等待线程。

这种设计使得AQS能够高效地处理线程的排队、阻塞和唤醒,是构建强大并发工具的基础。理解AQS是深入掌握Java并发编程的关键一步。

Java中的AQS(AbstractQueuedSynchronizer)详解 AQS是Java并发包java.util.concurrent.locks的核心框架,它为构建锁和同步器提供了一种底层的、高性能的、可扩展的基础。像ReentrantLock、Semaphore、CountDownLatch等常用的同步工具都是基于AQS构建的。 1. AQS的核心思想 AQS的核心思想非常简单:它维护了一个 volatile int类型的同步状态(state) 和一个 FIFO(先进先出)的线程等待队列(CLH队列的变体) 。 同步状态(state) :这是一个可以被并发访问的变量,它的具体含义由子类定义。例如: 在ReentrantLock中, state 表示锁被持有的次数(0表示未锁定,1表示被一个线程锁定,>1表示被同一个线程重入)。 在Semaphore中, state 表示当前可用的许可证数量。 在CountDownLatch中, state 表示还需要等待的计数。 等待队列 :这是一个双向链表,用于存放所有等待获取资源的线程。当线程尝试获取资源失败时,它会被封装成一个节点(Node)并加入队列尾部,进入等待状态。当持有资源的线程释放资源时,它会唤醒队列中的下一个(或所有)等待线程。 2. AQS的模板方法模式 AQS使用了 模板方法模式 。它定义了一系列获取和释放资源的“骨架”方法(如 acquire 和 release ),而将一些关键步骤的具体实现留给子类。子类需要重写以下几个 protected 方法来实现特定的同步语义: tryAcquire(int arg) :尝试以独占模式获取资源。成功返回true,失败返回false。 tryRelease(int arg) :尝试以独占模式释放资源。成功返回true,失败返回false。 tryAcquireShared(int arg) :尝试以共享模式获取资源。返回负数表示失败;0表示成功,但后续共享模式获取可能失败;正数表示成功,且后续共享模式获取可能成功。 tryReleaseShared(int arg) :尝试以共享模式释放资源。 isHeldExclusively() :当前同步器是否在独占模式下被线程占用。 3. 深入AQS的工作流程:以独占模式为例 我们以最典型的独占模式(如ReentrantLock)为例,详细拆解AQS的工作流程。 场景:线程A、B、C竞争一把基于AQS实现的锁。 步骤一:线程A尝试获取锁(lock.lock() -> acquire(1)) 调用 tryAcquire(1) :AQS首先会调用子类重写的 tryAcquire 方法。假设此时锁是自由的( state 为0)。 成功获取 :在 tryAcquire 方法中,子类会通过CAS操作尝试将 state 从0改为1。线程A的CAS操作成功, state 变为1,并且AQS会记录当前持有锁的线程是线程A( exclusiveOwnerThread = threadA )。 流程结束 : tryAcquire 返回true,线程A成功获取锁,继续执行其临界区代码。整个过程非常快速,没有涉及队列操作。 步骤二:线程B尝试获取锁(此时锁已被线程A持有) 调用 tryAcquire(1) :同样,AQS先调用 tryAcquire 。此时 state 为1,且持有线程不是B,所以 tryAcquire 方法返回false,获取失败。 加入等待队列 :由于 tryAcquire 失败,AQS会执行 addWaiter(Node.EXCLUSIVE) 方法。 将线程B封装成一个Node节点(模式为EXCLUSIVE)。 如果队列已经存在(尾节点tail不为null),则通过CAS操作快速地将新节点设置为尾节点。 如果队列不存在(如这是第一个等待线程)或CAS失败,则进入一个“自旋”循环,直到通过CAS操作成功将新节点添加到队列尾部。 在队列中自旋等待 :节点入队后,会调用 acquireQueued 方法。这是一个核心的自旋过程: 检查当前节点的前驱节点(prev)是不是头节点(head)。如果是,说明自己是队列中的第一个等待者,有资格再次尝试获取锁(调用 tryAcquire )。 此时线程A尚未释放锁,所以 tryAcquire 依然失败。 获取失败后,线程B会检查是否需要被挂起(park)。它会将前驱节点的 waitStatus (等待状态)设置为SIGNAL,表示“当你释放锁时,需要唤醒我”。 最后,调用 LockSupport.park(this) 将线程B挂起(进入WAITING状态),等待被唤醒。 步骤三:线程C也尝试获取锁 流程与线程B完全相同。线程C会被封装成Node,加入到等待队列中线程B的后面,然后被挂起。此时队列结构为: head -> [Node-B] -> [Node-C] (head是一个虚拟节点)。 步骤四:线程A释放锁(lock.unlock() -> release(1)) 调用 tryRelease(1) :AQS调用子类重写的 tryRelease 方法。该方法会将 state 减1。如果 state 变为0,表示锁已完全释放。 唤醒后继节点 :如果 tryRelease 返回true(释放成功),AQS会检查队列中是否有等待的线程(即头节点的 waitStatus 不为0)。 找到需要唤醒的节点 :AQS找到头节点的下一个节点(即线程B所在的节点)。 执行唤醒 unparkSuccessor :调用 LockSupport.unpark(node.thread) 来唤醒线程B。 步骤五:线程B被唤醒后继续竞争 线程B从之前被挂起的地方( LockSupport.park )继续执行。 它再次进入 acquireQueued 的自旋循环。 这次,它发现自己的前驱节点是头节点,于是再次调用 tryAcquire(1) 。 此时锁是自由的,线程B的CAS操作成功,将 state 从0改为1,并记录自己为锁的持有者。 线程B将自己设置为新的头节点(将原头节点出队)。 线程B成功获取锁,开始执行其临界区代码。 总结 AQS通过一个 state 变量和一個FIFO队列,精巧地管理了线程对共享资源的访问。其核心流程可以概括为: 获取资源 :先尝试 tryAcquire ,成功则直接返回。失败则将自己加入队列尾部,并自旋/等待,直到被前驱节点唤醒并成功获取资源。 释放资源 :执行 tryRelease ,成功后唤醒队列中下一个有效的等待线程。 这种设计使得AQS能够高效地处理线程的排队、阻塞和唤醒,是构建强大并发工具的基础。理解AQS是深入掌握Java并发编程的关键一步。