AQS概念
一、AQS 是什么?它的核心作用是什么?(必考基础)
- 答案:
- AQS 是
java.util.concurrent.locks
包下的一个抽象类。 - 它是构建锁(如
ReentrantLock
)和同步器(如Semaphore
,CountDownLatch
,ReentrantReadWriteLock
)的核心框架。 - 核心作用: 提供了一个基于 FIFO 等待队列的模板,用于实现阻塞锁和依赖特定状态(state)的同步器。它封装了线程排队、阻塞、唤醒的复杂逻辑,子类只需关注状态(state)的管理和获取/释放条件。
- AQS 是
二、AQS 的核心思想与关键组件(重中之重)
- 同步状态
state
(int 类型):- 是什么? AQS 管理的核心状态变量,子类决定其含义。例如:
ReentrantLock
: 表示锁的重入次数(0 表示未锁定)。Semaphore
: 表示剩余的许可证数量。CountDownLatch
: 表示还需要倒数的计数。
- 如何访问? 通过
getState()
,setState(int newState)
,compareAndSetState(int expect, int update)
(CAS 操作) 方法进行原子操作。 - 为什么是
int
? 足够表达大多数同步场景的状态(重入次数、资源数等),且 CAS 操作高效。子类也可以使用位运算管理多个状态(如ReentrantReadWriteLock
)。
- 是什么? AQS 管理的核心状态变量,子类决定其含义。例如:
- FIFO 等待队列 (CLH 队列的变种):
- 是什么? 一个双向链表,用于存放等待获取资源的线程。
- 节点
Node
: 队列中的每个节点代表一个等待线程。关键属性:waitStatus
: 节点状态(CANCELLED
,SIGNAL
,CONDITION
,PROPAGATE
)。SIGNAL
是最重要的,表示该节点的后继节点需要被唤醒。prev
,next
: 指向前驱和后继节点的指针。thread
: 等待线程的引用(可能为 null)。nextWaiter
: 指向下一个在条件队列(ConditionObject
)上等待的节点,或用于区分独占模式和共享模式。
head
和tail
: 队列的头尾指针(头节点是虚节点,不代表任何线程)。
- 模板方法模式 (Template Method Pattern):
- AQS 的核心设计模式。 AQS 定义了获取/释放资源的主流程(如
acquire(int arg)
,release(int arg)
),但将关键决策留给子类实现。 - 子类需要重写的核心 protected 方法:
tryAcquire(int arg)
: 独占模式下尝试获取资源。成功返回 true,失败返回 false。tryRelease(int arg)
: 独占模式下尝试释放资源。成功返回 true,失败返回 false。tryAcquireShared(int arg)
: 共享模式下尝试获取资源。返回值 >=0 表示成功(值代表剩余资源数),<0 表示失败。tryReleaseShared(int arg)
: 共享模式下尝试释放资源。如果释放后可能唤醒后续等待者则返回 true。isHeldExclusively()
: 当前线程是否独占持有资源(主要用于Condition
)。
- AQS 的核心设计模式。 AQS 定义了获取/释放资源的主流程(如
三、AQS 的核心流程(关键流程要清晰)
面试官会让你描述主要流程,特别是 acquire
和 release
:
- 独占模式获取资源 (
acquire(int arg)
):java复制下载public final void acquire(int arg) { if (!tryAcquire(arg) && // 1. 子类尝试直接获取 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 2. 获取失败,将线程包装成独占节点加入队列尾部 selfInterrupt(); // 3. 如果在等待过程中被中断过,补上中断标记 }- 步骤详解:
- 调用子类实现的
tryAcquire(arg)
尝试直接获取资源(非阻塞)。 - 如果失败:
addWaiter(Node.EXCLUSIVE)
:将当前线程包装成一个独占模式(Node.EXCLUSIVE
) 的节点,快速 CAS 尝试入队尾。如果失败(竞争),则调用enq(node)
方法自旋 CAS 直到成功入队尾。acquireQueued(final Node node, int arg)
:节点入队后,在此方法中自旋:- 检查前驱节点是否是
head
(说明马上轮到自己了),如果是则再次调用tryAcquire(arg)
尝试获取。 - 如果获取成功,将自己设为新的
head
(原head
出队),返回。 - 如果获取失败 或 前驱不是
head
:- 调用
shouldParkAfterFailedAcquire(p, node)
:检查并设置前驱节点的waitStatus
为SIGNAL
(表示“我挂了请唤醒我”),并返回是否需要阻塞。 - 如果需要阻塞,调用
parkAndCheckInterrupt()
:使用LockSupport.park()
阻塞当前线程。线程被唤醒后,检查是否被中断过。
- 调用
- 检查前驱节点是否是
- 如果在阻塞等待过程中被中断过(
Thread.interrupted()
返回 true),则在返回上层后调用selfInterrupt()
补上中断状态(AQS 在等待过程中不响应中断,但会记录中断状态,获取到资源后才响应)。
- 调用子类实现的
- 步骤详解:
- 独占模式释放资源 (
release(int arg)
):java复制下载public final boolean release(int arg) { if (tryRelease(arg)) { // 1. 子类尝试释放资源 Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); // 2. 唤醒头节点的有效后继节点 return true; } return false; }- 步骤详解:
- 调用子类实现的
tryRelease(arg)
尝试释放资源。 - 如果释放成功(资源完全释放,state==0):
- 检查头节点
h
是否存在且waitStatus != 0
(通常为SIGNAL
,表示它有后继需要唤醒)。 - 调用
unparkSuccessor(h)
:找到头节点h
后面第一个waitStatus <= 0
(非取消状态)的节点,使用LockSupport.unpark(s.thread)
唤醒该节点对应的线程。
- 检查头节点
- 调用子类实现的
- 步骤详解:
- 共享模式获取/释放 (
acquireShared
/releaseShared
):- 流程与独占模式类似,主要区别:
- 节点类型是
Node.SHARED
。 tryAcquireShared
返回值 >=0 表示成功(值代表剩余资源数)。- 在
doAcquireShared
中,成功获取后(通常是前驱是 head 且tryAcquireShared
>=0),不仅将自己设为新 head,还会调用setHeadAndPropagate(node, r)
尝试唤醒后续的共享节点(传播性,如Semaphore
释放多个许可证时)。 releaseShared
释放后必须调用doReleaseShared()
确保唤醒后继(即使tryReleaseShared
返回 true 也可能需要唤醒,因为共享模式允许多个线程同时获取)。
- 节点类型是
- 流程与独占模式类似,主要区别:
四、关键机制与特性(深入理解点)
- 为什么是双向链表 (CLH 变种)?
- 主要为了高效处理取消操作。当一个节点(线程)取消等待(超时或中断)时,需要将自己从队列中移除。双向链表可以方便地修改
prev
和next
指针,确保移除操作 O(1) 完成。单向链表移除中间节点需要遍历。
- 主要为了高效处理取消操作。当一个节点(线程)取消等待(超时或中断)时,需要将自己从队列中移除。双向链表可以方便地修改
waitStatus
的作用:CANCELLED (1)
: 节点因超时或中断取消。需要被移除。SIGNAL (-1)
: 最重要! 表示该节点的后继节点需要被唤醒。节点在阻塞前,必须确保其前驱的waitStatus
是SIGNAL
。CONDITION (-2)
: 节点在条件队列(ConditionObject
)中等待。PROPAGATE (-3)
: 仅用于共享模式头节点。表示下一次acquireShared
应该无条件传播(唤醒后续共享节点)。解决共享释放的竞争问题。0
: 初始状态。
- 公平锁 vs 非公平锁 (以
ReentrantLock
为例):- 公平锁: 严格按照 FIFO 队列顺序获取锁。
tryAcquire
实现:如果 state==0 且队列中没有其他等待线程(!hasQueuedPredecessors()
),才尝试 CAS 获取。 - 非公平锁: 新来的线程可以“插队”,直接尝试 CAS 获取 state,失败后再入队。
tryAcquire
实现:直接尝试 CAS 获取 state,不管队列情况。 - 优缺点: 公平锁避免饥饿,但吞吐量通常低于非公平锁(唤醒线程开销大)。非公平锁吞吐量高,但可能导致线程饥饿。
- 公平锁: 严格按照 FIFO 队列顺序获取锁。
Condition
条件队列 (ConditionObject
):- AQS 内部类,实现
Condition
接口。 - 核心: 每个
ConditionObject
维护一个单向的条件等待队列。 await()
: 释放锁 -> 创建CONDITION
节点加入条件队列 -> 阻塞。signal()
: 将条件队列头节点(等待最久的)转移到 AQS 主队列尾部,等待被唤醒竞争锁。signalAll()
: 转移条件队列所有节点到主队列。- 关键点: 一个 AQS 实例(锁)可以有多个
ConditionObject
(多个等待条件)。
- AQS 内部类,实现
五、典型应用(知道如何基于 AQS 实现)
面试官可能让你简述某个同步器如何利用 AQS:
ReentrantLock
(独占):state
表示锁的重入计数(0 未锁定)。tryAcquire
: 如果 state==0 则 CAS 尝试获取(非公平)或 检查队列后再获取(公平);如果 state>0 且当前线程是持有者,则 state++(重入)。tryRelease
: state–;只有当 state==0 时才真正释放(返回 true)。
Semaphore
(共享):state
表示可用许可证数量。tryAcquireShared
: 计算available = state - acquires
。如果available < 0
或 CAS 设置state = available
成功,则返回available
(<0 失败,>=0 成功)。tryReleaseShared
: 自旋 CAS 增加 state (state += releases
),返回 true(总是可能唤醒等待者)。
CountDownLatch
(共享):state
表示需要倒数的计数。tryAcquireShared
: 只要state == 0
就返回 1(成功),否则返回 -1(失败)。等待线程调用await()
->acquireSharedInterruptibly(1)
。tryReleaseShared
: 每次countDown()
调用 CAS 减少 state。当 state 被减到 0 时,返回 true(唤醒所有等待线程)。
ReentrantReadWriteLock
(组合):- 使用一个 AQS 实例。巧妙利用
state
(int) 的高 16 位表示读锁(共享)计数,低 16 位表示写锁(独占)计数。 - 实现更复杂,需要处理读写互斥、读读共享、写写互斥、写锁降级等。
- 使用一个 AQS 实例。巧妙利用
六、如何准备
- 重点掌握: 核心思想(模板方法、state、CLH队列)、独占模式
acquire/release
流程、waitStatus
(特别是SIGNAL
)、公平/非公平区别。 - 理解典型应用: 至少能清晰说出
ReentrantLock
和Semaphore
/CountDownLatch
是如何基于 AQS 实现的。 - 动手实践: 尝试阅读
ReentrantLock
的源码(特别是NonfairSync
/FairSync
和Sync
类),理解它们与 AQS 的协作。 - 清晰表述: 能用流程图或简洁语言描述
acquire
和release
的主要步骤。 - 区分概念: AQS 本身不是锁,它是构建锁的框架;
Lock
接口定义了锁的行为,ReentrantLock
等是具体实现,它们依赖 AQS。 - 了解即可(非重点): 条件队列(
ConditionObject
)的详细流程、PROPAGATE
状态解决的极端情况、复杂的取消逻辑。秋招深度通常不会挖到这么细。
ReentrantReadWriteLock
是解决“读多写少”场景的核心方案。
常与synchronized
、ReentrantLock
对比,例如:
- “为什么读写锁比互斥锁更适合缓存场景?”
- “读写锁和StampedLock的区别是什么?
高频考点梳理
1. 核心特性(100%出现)
- 规则:读读共享、读写互斥、写写互斥。
- 适用场景:读频率 >> 写频率(如缓存、数据库查询)。
- 性能优势:读操作并发执行,减少线程阻塞,提升吞吐量(对比
synchronized
)。
2. ReentrantReadWriteLock 实现细节(80%出现)
- 公平性:默认非公平锁(吞吐量高),可设置为公平锁(防止饥饿但性能低)。
- 可重入性:同一线程可重复获取读锁或写锁。
- 锁降级:写锁 → 读锁(允许),读锁 → 写锁(禁止)
典型用例:先持写锁修改数据,再持读锁保证后续读取一致性。
3. 缺陷与解决方案(60%出现)
- 锁饥饿问题:持续读锁导致写线程无法获取锁(可通过公平锁缓解)。
- 锁升级不支持:持有读锁时无法直接升级为写锁(需先释放读锁)。
- 替代方案:
StampedLock
的乐观读(无锁快照)解决读写竞争问题。
4. 场景应用题(40%出现)
- 缓存系统设计:
如何用ReentrantReadWriteLock
实现线程安全的缓存(参考CacheDemo
代码)。 - 数据库连接池:
读连接可共享,写连接需独占。
如何准备
- 理解核心思想:明确读写分离的适用场景(读 >> 写),并能解释其性能优势。
- 掌握锁降级流程:写锁 → 读锁的代码实现(需在释放写锁前获取读锁)。
- 对比其他锁:
- 与
StampedLock
对比:后者通过乐观读避免写线程饥饿。 - 与
synchronized
对比:读写锁在低竞争时性能无优势,高并发读时显著优化。
- 与
- 代码实操:手写缓存类(如
MyCache
),演示读写锁的正确使用。