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表示还需要等待的计数。
- 在ReentrantLock中,
- 等待队列:这是一个双向链表,用于存放所有等待获取资源的线程。当线程尝试获取资源失败时,它会被封装成一个节点(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状态),等待被唤醒。
- 检查当前节点的前驱节点(prev)是不是头节点(head)。如果是,说明自己是队列中的第一个等待者,有资格再次尝试获取锁(调用
步骤三:线程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并发编程的关键一步。