AQS 是 java.util.concurrent
包中构建锁和同步器的核心框架,理解其原理对于掌握 Java 并发工具至关重要。
AQS 的核心概念回顾:
- 同步状态 (State): 一个由
volatile int
类型的变量表示的共享资源的状态。子类通过 CAS 操作来管理这个状态。 - FIFO 等待队列 (CLH 变体): 当线程获取同步状态失败时,会被封装成节点(Node)加入到队列尾部并阻塞。当状态释放时,按照 FIFO 原则唤醒队列中的线程。
AQS 的内部结构:
AQS 内部维护着:
volatile int state
(同步状态): 所有同步逻辑的核心。不同的同步器(如ReentrantLock
、Semaphore
)会赋予state
不同的含义。transient volatile Node head
(头节点): 指向等待队列的头部。头节点通常是一个“虚节点”,不关联任何实际等待的线程。transient volatile Node tail
(尾节点): 指向等待队列的尾部。新的等待线程会被添加到尾部。
AQS 的等待队列是一个虚拟的双向链表,它不是通过显式的 next
指针在所有节点间直接连接,而是通过每个节点的 prev
和 nextWaiter
字段以及与前驱节点的隐式连接来维护等待关系。
AQS 的同步模式:
AQS 支持两种主要的同步模式,子类可以选择实现一种或两种:
-
独占模式 (Exclusive): 同一时刻只允许一个线程持有同步状态。
ReentrantLock
是独占锁的典型实现。- 通过
tryAcquire(int arg)
尝试获取独占锁。 - 通过
tryRelease(int arg)
尝试释放独占锁。 isHeldExclusively()
判断当前线程是否持有独占锁。
- 通过
-
共享模式 (Shared): 同一时刻允许多个线程持有同步状态。
Semaphore
和CountDownLatch
是共享锁的典型实现。ReentrantReadWriteLock
的读锁也是共享的。- 通过
tryAcquireShared(int arg)
尝试获取共享锁,返回值大于等于 0 表示成功。 - 通过
tryReleaseShared(int arg)
尝试释放共享锁,成功返回true
。
- 通过
AQS 的核心操作流程:
无论是独占模式还是共享模式,AQS 的核心操作都围绕着获取和释放同步状态展开,并涉及到线程的阻塞和唤醒以及队列的管理。
1. 获取同步状态 (acquire(int arg)
- 独占模式):
- 尝试直接获取同步状态 (
tryAcquire(arg)
)。如果成功,则当前线程成为持有锁的线程,方法返回。 - 如果获取失败,则将当前线程封装成一个
Node
并添加到等待队列的尾部 (addWaiter(Node mode)
,mode
通常为Node.EXCLUSIVE
)。 - 当前线程进入阻塞状态 (
parkAndUnpark(Node node)
),直到被前驱节点释放锁后唤醒。 - 被唤醒后,再次尝试获取同步状态 (
tryAcquire(arg)
)。如果仍然失败,则继续阻塞。 - 在阻塞和唤醒的过程中,节点会根据前驱节点的状态来判断是否需要阻塞,以及在释放锁后是否需要唤醒后继节点。
2. 释放同步状态 (release(int arg)
- 独占模式):
- 尝试释放同步状态 (
tryRelease(arg)
)。如果释放成功,则检查等待队列中是否有需要唤醒的后继节点。 - 如果存在后继节点且其状态表示需要被唤醒 (
Node.SIGNAL
),则唤醒该后继节点 (unparkSuccessor(Node node)
,通常唤醒头节点的后继节点)。
3. 获取共享同步状态 (acquireShared(int arg)
- 共享模式):
- 尝试获取共享同步状态 (
tryAcquireShared(arg)
)。如果返回值大于等于 0,表示获取成功,方法返回。 - 如果获取失败,则将当前线程封装成一个
Node
并以共享模式 (Node.SHARED
) 添加到等待队列的尾部。 - 当前线程进入阻塞状态,直到被其他持有共享锁的线程释放后唤醒。
- 被唤醒后,再次尝试获取共享同步状态 (
tryAcquireShared(arg)
)。如果仍然失败,则继续阻塞。 - 共享锁的唤醒可能会涉及到信号传播 (signal propagation),即一个共享锁的释放可能会唤醒队列中的多个后继节点,直到没有更多的线程可以获取到共享锁为止。
4. 释放共享同步状态 (releaseShared(int arg)
- 共享模式):
- 尝试释放共享同步状态 (
tryReleaseShared(arg)
)。如果释放成功,则需要唤醒等待队列中处于共享模式的后继节点,这些节点可能也能够获取到共享锁(通过信号传播)。
AQS 队列的特点 (CLH 队列的变体):
- 虚拟双向链表: 虽然没有显式的
next
指针直接连接所有等待节点,但每个节点通过prev
指向其前驱节点,并且头节点和尾节点维护着队列的边界。节点间的唤醒和状态传递依赖于这种隐式的连接。 - head 和 tail 指针:
head
指向一个通常不持有实际线程的虚节点,而tail
指向队列中最后一个等待的节点。 - Node 结构: 每个
Node
包含:waitStatus
: 节点的等待状态,例如SIGNAL
(需要被唤醒)、CONDITION
(在条件队列中等待)、PROPAGATE
(需要向后传播唤醒信号) 等。prev
: 指向前驱节点。nextWaiter
: 指向下一个等待相同条件或相同模式(独占/共享)的节点。thread
: 关联的等待线程。
- CAS 操作: AQS 大量使用了 Compare-and-Swap (CAS) 原子操作来安全地更新
state
、head
和tail
等共享变量,保证并发环境下的线程安全。
AQS 的重要方法 (供子类实现):
子类需要根据自身的同步语义实现以下一个或多个方法:
tryAcquire(int arg)
tryRelease(int arg)
tryAcquireShared(int arg)
tryReleaseShared(int arg)
isHeldExclusively()
AQS 的模板方法 (供子类调用):
AQS 提供了一系列 final
修饰的模板方法,子类可以直接调用来实现同步逻辑,例如:
acquire(int arg)
acquireInterruptibly(int arg)
(支持中断)tryAcquireNanos(int arg, long nanosTimeout)
(支持超时)release(int arg)
acquireShared(int arg)
acquireSharedInterruptibly(int arg)
tryAcquireSharedNanos(int arg, long nanosTimeout)
releaseShared(int arg)
AQS 与 ConditionObject:
AQS 还提供了创建 ConditionObject
的能力。ConditionObject
可以与一个独占锁关联,提供了 wait()
、signal()
和 signalAll()
等方法,用于实现更灵活的线程等待和通知机制。ConditionObject
内部维护着一个独立的等待队列。
总结 AQS 的核心要点:
- AQS 是构建同步器的基石,提供了一个统一的框架。
- 它通过维护一个同步状态 (
state
) 和一个 FIFO 等待队列来管理并发访问。 - 支持独占和共享两种同步模式。
- 依赖于子类实现特定的
tryAcquire
和tryRelease
等方法来定义同步语义。 - 使用 CAS 操作保证并发安全。
- 提供了模板方法简化同步器的实现。
- 支持条件变量 (
ConditionObject
) 实现更复杂的等待/通知机制。
理解 AQS 的原理是深入学习 java.util.concurrent
包中各种锁和同步工具的关键。例如,ReentrantLock
、CountDownLatch
、Semaphore
等都是基于 AQS 实现的,它们的行为和特性都与 AQS 的基本机制密切相关。