对AQS的详解
目录
一、AQS 是什么?
二、核心原理
1. 状态变量 (state)
2. CLH 队列 (同步队列)
3. 两种资源共享模式
三、设计与关键方法:模板方法模式
需要子类实现的方法 (Protected)
重要的模板方法 (Public Final)
四、工作流程详解 (以独占模式为例)
获取锁 (acquire(int arg))
释放锁 (release(int arg))
五、实战:看 ReentrantLock 如何基于 AQS 实现
总结
一、AQS 是什么?
AQS 是一个用于构建锁和同步器的框架。它提供了一个基于 FIFO 等待队列的底层同步机制,开发者通过继承 AQS 并实现其少量的模板方法,就可以轻松地构建出各种功能的同步器。
它的核心思想是:
-
如果被请求的共享资源是空闲的,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。
-
如果被请求的共享资源被占用,那么就需要一套线程阻塞等待、被唤醒时锁分配的机制。这个机制 AQS 是用 CLH 队列锁 来实现的,即将暂时获取不到锁的线程加入到队列中。
二、核心原理
AQS 的核心可以概括为三个部分:
-
一个 volatile 的整型状态变量 (state)
-
一个 FIFO 线程等待队列 (CLH队列的变体)
-
两种资源共享模式:独占 (Exclusive) 和 共享 (Share)
1. 状态变量 (state)
这是一个用 volatile
修饰的 int
成员变量,是 AQS 的灵魂所在。锁的概念很大程度上就是对 state 的操作。
状态含义由子类定义:AQS 不关心 state 的具体含义,完全由其子类根据需求决定。例如:
- ReentrantLock:
state
表示重入次数。0 表示锁空闲,>=1 表示被持有,>1 表示被同一线程重入。 - Semaphore:
state
表示剩余的许可证数量。 - CountDownLatch:
state
表示还需要等待的计数。
所有操作都是原子性的:对 state 的修改都通过 CAS (Compare-And-Swap) 操作来保证原子性和可见性。
2. CLH 队列 (同步队列)
这是一个双向链表结构的队列,用于存放所有等待获取资源的线程。当线程申请资源失败时,AQS 会将其封装成一个 Node 节点,并将其加入队列尾部,同时阻塞该线程(使用 LockSupport.park()
)。当持有资源的线程释放资源时,会唤醒队列中的下一个节点,使其重新尝试获取资源。
Node 节点的主要属性:
-
thread
: 等待的线程 -
waitStatus
: 节点的状态(如CANCELLED
,SIGNAL
,CONDITION
等) -
prev
,next
: 前驱和后继指针 -
nextWaiter
: 用于表示节点是共享模式还是独占模式
3. 两种资源共享模式
-
独占模式 (Exclusive): 资源一次只允许一个线程访问。例如:
ReentrantLock
。 -
共享模式 (Share): 资源允许多个线程同时访问。例如:
Semaphore
,CountDownLatch
。
不同的模式决定了线程在获取和释放资源时的行为,尤其是在唤醒等待队列中的线程时,共享模式可能会一次性唤醒多个节点。
三、设计与关键方法:模板方法模式
AQS 使用了模板方法模式。它定义了顶级逻辑的骨架(如入队、出队),而将一些关键步骤的实现推迟到子类中。
需要子类实现的方法 (Protected)
这些方法是构建同步器的核心,AQS 会在其内部逻辑中调用它们:
boolean tryAcquire(int arg)
: 独占式获取资源。成功返回 true,失败返回 false。boolean tryRelease(int arg)
: 独占式释放资源。成功返回 true,失败返回 false。int tryAcquireShared(int arg)
: 共享式获取资源。负数表示失败;0 表示成功,但后续获取可能失败;正数表示成功,且后续获取可能成功。boolean tryReleaseShared(int arg)
: 共享式释放资源。如果释放后允许后续等待节点获取资源,则返回 true,否则返回 false。boolean isHeldExclusively()
: 当前同步器是否被当前线程独占持有。
重要的模板方法 (Public Final)
这些方法是提供给使用者(或子类)的 API,逻辑已经由 AQS 实现好,通常是 final
的。
获取资源
acquire(int arg)
: 独占式获取资源,忽略中断。这是lock()
的底层实现。acquire
->tryAcquire
-> (如果失败)addWaiter
->acquireQueued
。acquireInterruptibly(int arg)
: 同上,但响应中断。acquireShared(int arg)
: 共享式获取资源,忽略中断。acquireSharedInterruptibly(int arg)
: 同上,但响应中断。
释放资源
release(int arg)
: 独占式释放资源。release
-> tryRelease
-> unparkSuccessor
。
releaseShared(int arg)
: 共享式释放资源。
其他
boolean hasQueuedThreads()
: 是否有线程在等待。
Collection<Thread> getQueuedThreads()
: 获取等待线程集合。
四、工作流程详解 (以独占模式为例)
获取锁 (acquire(int arg)
)
-
tryAcquire: 调用子类实现的
tryAcquire(arg)
方法尝试直接获取资源。 -
成功: 如果成功,流程结束,线程继续执行。
-
失败: 如果失败,AQS 会将当前线程包装成一个独占模式的 Node 节点,并通过
addWaiter(Node.EXCLUSIVE)
方法将其添加到同步队列的尾部。 -
acquireQueued: 然后调用
acquireQueued
方法,让节点以自旋(循环) 的方式尝试获取资源。-
在自旋中,只有当前节点的前驱节点是头节点 (head) 时,它才有资格再次尝试调用
tryAcquire
获取资源。 -
如果获取成功,则将当前节点设置为新的头节点,原头节点出队。
-
如果获取失败,则判断是否需要阻塞当前线程(通过
shouldParkAfterFailedAcquire
方法检查前驱节点的状态)。如果需要,则调用parkAndCheckInterrupt()
方法,底层使用LockSupport.park(this)
阻塞当前线程。
-
-
阻塞与唤醒: 线程被阻塞,等待被唤醒。当持有锁的线程调用
release
释放资源时,会唤醒头节点的后继节点。被唤醒的线程会从parkAndCheckInterrupt()
中返回,并再次进行自旋尝试获取锁。
释放锁 (release(int arg)
)
tryRelease: 调用子类实现的
tryRelease(arg)
方法尝试释放资源。成功: 如果释放成功(例如,state 变为 0),则获取当前头节点
h
。unparkSuccessor: 如果头节点存在且
waitStatus != 0
(表示它有后继节点需要唤醒),则调用unparkSuccessor(h)
方法,找到头节点后面第一个未被取消的节点,并使用LockSupport.unpark(s.thread)
唤醒该节点对应的线程。
五、实战:看 ReentrantLock 如何基于 AQS 实现
ReentrantLock
内部有一个同步器 Sync
,它继承自 AQS。
state 的含义: 锁的重入次数。
tryAcquire:
protected final boolean tryAcquire(int acquires) {final Thread current = Thread.currentThread();int c = getState(); // 获取当前状态if (c == 0) { // 锁空闲if (!hasQueuedPredecessors() && // 公平锁才会检查是否有前驱节点在等待compareAndSetState(0, acquires)) { // CAS 抢锁setExclusiveOwnerThread(current); // 成功,设置当前线程为锁持有者return true;}}else if (current == getExclusiveOwnerThread()) { // 锁已被持有,且是当前线程重入int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc); // 直接修改 state,无需 CAS,因为就是当前线程return true;}return false; // 获取失败
}
tryRelease:
protected final boolean tryRelease(int releases) {int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;if (c == 0) { // 重入次数减为0,才真正释放free = true;setExclusiveOwnerThread(null);}setState(c); // 更新 statereturn free;
}
当我们调用 lock.lock()
时,底层就是调用了 sync.acquire(1)
,进而触发了上述流程。
总结
特性 | 描述 |
---|---|
核心 | 一个状态变量 state + 一个 FIFO 线程等待队列 |
设计模式 | 模板方法模式。子类实现 tryAcquire , tryRelease 等方法来定义资源获取和释放的规则 |
关键操作 | 通过 CAS 操作来原子性地修改 state |
线程阻塞/唤醒 | 使用 LockSupport.park() 和 LockSupport.unpark() |
优点 | 极大地简化了同步器的开发,将复杂的线程排队、阻塞、唤醒等底层操作封装好,开发者只需关注对 state 的管理 |
理解 AQS,就等于拿到了理解 Java 并发包中大部分同步工具实现原理的钥匙。它是 Java 并发大师 Doug Lea 的杰作,是 Java 并发编程中一座重要的里程碑。