AQS原理
核心:state状态值,FIFO双端队列,CAS抢占式机制
AQS(AbstractQueuedSynchronizer)是Java并发包的核心框架,用于管理多线程竞争共享资源的同步问题。通俗理解,它像奶茶店的排队系统:一个收银台(共享资源)、排队队列(线程管理)、叫号机(状态控制)。下面通过生活场景拆解其原理:
🧋Java并发包中的AbstractQueuedSynchronizer是构建锁和其他同步器(如Semaphore、CountDownLatch等)的核心框架。理解其原理对掌握Java并发至关重要。以下是其核心原理的详细解析:
1. 核心设计思想
- 状态管理:
- 维护一个volatile int state状态变量,代表共享资源的状态。
- 在ReentrantLock中:state = 0表示锁空闲,state > 0表示被线程持有(可重入时>1)。
- 在Semaphore中:state表示可用许可证的数量。
- 在CountDownLatch中:state表示需要等待的事件(计数器)数量。
- 线程排队等待:
- 当线程请求资源失败(state不满足条件),会被构造成一个Node节点加入一个FIFO双向队列(CLH队列的变体)中排队等待,并在适当的时候被唤醒尝试获取资源。
- Node封装了等待线程、状态(CANCELLED、SIGNAL、CONDITION、PROPAGATE)等信息。
- 队列是抽象的,不包含实际数据对象,只有对节点关系的管理。
2. 关键操作原理解析
核心围绕acquire(获取资源)和release(释放资源)操作。
获取资源 (acquire(int arg))
1.tryAcquire(arg): 子类必须实现此模板方法!根据自定义策略尝试直接获取资源(操作state)。成功返回true,线程继续执行。
2.失败入队:
a.如果tryAcquire失败(返回false):将当前线程包装成一个Node节点。
i.CAS入队尾: 使用compareAndSetTail以无锁(乐观锁)方式将该节点安全地添加到队列尾部。
ii.acquireQueued(final Node node, int arg): 进入此方法,核心在自旋循环中:
1.检查前驱: 检查新加入节点(或其有效前置节点)的前一个节点p是否是head(队列中第一个等待线程)。
2.尝试获取: 如果是head,则调用tryAcquire(arg)再次尝试获取资源(给刚入队的线程一次机会)。
3.获取成功: 将当前节点设置为新的head(原head出队),返回中断标记。
4.获取失败或被唤醒后检查:
a.调用shouldParkAfterFailedAcquire(Node pred, Node node):检查前置节点状态。
b.如果pred.waitStatus == SIGNAL(表示pred有义务在释放时唤醒后续节点),则当前线程可以安全阻塞park。
c.否则(如CANCELLED状态),跳过无效的前驱节点直到找到有效前驱,并将其waitStatus设置为SIGNAL。
d.调用parkAndCheckInterrupt():使用LockSupport.park(this)阻塞当前线程。
e.线程在此处挂起。
5.线程唤醒: 被前驱节点释放资源时唤醒或响应中断唤醒。
6.检查中断: 判断唤醒是否为中断引起,并返回中断状态。
7.处理中断: 如果在等待过程中被中断,acquire通常会将中断再标记(selfInterrupt),以符合Lock API的约定(不响应中断,但保留标记)。
释放资源 (release(int arg))
1.tryRelease(arg):
a.子类必须实现此模板方法!尝试释放资源(操作state)。
b.成功返回true。
2.唤醒后继:
a.如果tryRelease成功:检查头节点h(通常是持有资源的节点)。
b.如果h.waitStatus != 0(通常为SIGNAL,表示后继节点在等待唤醒):
i.调用unparkSuccessor(Node node):尝试将节点的waitStatus重置为0。
ii.找到队列中第一个未取消的有效后继节点s(从尾向头找最前面的有效节点是一种优化)。
iii.如果s不为null,使用LockSupport.unpark(s.thread)唤醒该后继节点的线程。
iv.该线程在parkAndCheckInterrupt()处被唤醒,继续在acquireQueued的自旋循环中尝试获取资源。
3. 模式
- 独占模式 (Exclusive):
- 同一时间只有一个线程能获取资源(如ReentrantLock)。
- 核心方法:acquire(int arg), acquireInterruptibly(int arg), tryAcquireNanos(int arg, long nanosTimeout), release(int arg)。
- 共享模式 (Shared):
- 多个线程可以同时获取资源(如Semaphore, CountDownLatch)。
- 核心方法:acquireShared(int arg), acquireSharedInterruptibly(int arg), tryAcquireSharedNanos(int arg, long nanosTimeout), releaseShared(int arg)。
- 区别主要在tryAcquireShared需要返回剩余可用数量,并且唤醒会传播(见PROPAGATE状态)。
4. 核心特性与价值
- 高效的等待队列管理: FIFO队列保证了公平性(默认是非公平,但子类可实现公平策略)。
- 低竞争开销: 通过自旋(少量重试)和CAS操作,在入队和状态修改时避免了重量级锁的开销。
- 状态与队列解耦: state表示资源状态,队列管理排队线程。获取和释放操作操作state,状态变化则驱动队列中节点的唤醒/阻塞。
- 条件变量支持: ConditionObject内部类(每个锁可关联多个条件队列)实现了条件等待/通知机制。
- 高度抽象与可扩展:
- 模板方法模式是其核心。子类只需实现:tryAcquire/tryRelease(独占)tryAcquireShared/tryReleaseShared(共享)来控制资源获取和释放的具体语义(例如,判断state是否满足获取条件,state如何变化)。而资源获取失败时的排队、阻塞、唤醒逻辑由AQS父类处理。
5. 典型应用
- ReentrantLock: 实现了公平锁和非公平锁,核心是实现了AQS的独占方法,并控制tryAcquire策略(非公平锁会“插队”)。
- Semaphore: 实现了tryAcquireShared(检查state>0并减1)和tryReleaseShared(state加1)。
- CountDownLatch: 实现了tryAcquireShared(state==0时获取成功)和tryReleaseShared(减state,当减到0时返回true触发唤醒)。
- ReentrantReadWriteLock: 读写锁,利用AQS的state高16位表示读锁计数,低16位表示写锁计数。
- SynchronousQueue: 使用AQS管理线程阻塞/唤醒。
总结
AQS提供了一套通用的模板用于构建同步器。其本质是:通过一个原子状态值state代表资源状态,结合一个等待线程的FIFO队列实现资源的获取、排队等待和释放唤醒机制。 开发者只需关注如何操作state(通过实现几个模板方法)即可定义复杂的同步规则,极大地简化了高性能、低延迟并发工具的实现难度。理解AQS是深入Java并发编程的必经之路。
AQS释放锁机制以及通知机制
AQS 的释放锁机制和通知机制是其高效实现同步的核心,下面分别进行深入解析:
一、释放锁机制 (release()方法)
这是独占模式下释放资源的流程:
1.tryRelease(int arg)尝试释放资源:
a.由子类具体实现(如 ReentrantLock.Sync)。
b.核心: 操作 state变量。通常是递减或重置 state。在 ReentrantLock中:state = state - releases(可重入锁需要多次释放直到 state == 0才算完全释放)。
c.返回值: 如果操作后资源已被完全释放(state == 0),则返回 true。只有当返回 true时,AQS 才会触发唤醒后继线程。对于可重入锁,只有最后一次释放才会返回 true。
2.检查并唤醒后继节点 (unparkSuccessor(Node node)):
a.如果 tryRelease返回 true,说明资源完全释放,需要唤醒队列中合适的等待线程。
b.获取当前头节点 h(持有资源的节点正在释放资源)。
c.检查头节点的 waitStatus:
i.如果 h.waitStatus < 0(通常是 SIGNAL (-1)或 PROPAGATE (-3)):
1.重置状态: 使用 CAS 将头节点的 waitStatus设为 0 (减少不必要的唤醒信号)。
ii.如果 h.waitStatus >= 0(可能是 0或 CANCELLED (1)),说明没有有效后继节点需要唤醒,直接跳过。
d.寻找有效后继节点:首先尝试:从 h.next(后继)开始向后查找。
e.关键优化:
i.如果 s == null || s.waitStatus > 0(表示后继节点无效或被取消):
1.从队尾向前遍历 (tail→ head) 找到队列中 waitStatus <= 0的最前面的有效节点。
2.为什么从尾向前?
a.因为节点入队是“设置新节点的 prev = oldTail” + “CAS更新 tail = 新节点”两步操作。
b.node.next = nextNode的链接是在之后设置(或由唤醒线程设置)。
c.如果在并发场景下从前往后找,可能 next指针还未正确设置。从后往前利用已经稳定的 prev指针则能保证总能找到完整的队列。
f.唤醒线程:
i.找到有效后继节点 s后,调用 LockSupport.unpark(s.thread)唤醒该节点关联的线程。
ii.该线程从之前在 acquireQueued()中的 LockSupport.park()处被唤醒。
iii.被唤醒的线程会重新在 acquireQueued()的自旋循环中尝试获取资源 (tryAcquire())。
iv.由于资源已被释放,它有很大概率(在非公平锁下还需竞争)能成功获取锁,成为新的 head节点并继续执行。
释放锁机制核心思想:
1.状态更新在前: 先通过 tryRelease安全释放资源(更新 state)。
2.精准唤醒: 一旦确认资源可被获取,按照 FIFO 原则 唤醒队列中 第一个有效且未被取消 的等待线程(头节点的后继)。
3.低开销唤醒: 使用 unpark()精确唤醒一个线程,避免了不必要的线程上下文切换开销。
4.竞争重新开始: 被唤醒的线程需要重新尝试获取锁,这保证了 非公平锁的“插队”特性(即使有新线程此时来抢锁,也可能成功)。
共享模式 (releaseShared()):
1.tryReleaseShared(int arg):
a.子类实现释放资源逻辑(如 Semaphore中是 state = state + releases),可能需要循环 CAS。
2.如果 tryReleaseShared返回 true(表示资源释放成功且状态变化可能允许其他线程获取),调用 doReleaseShared()。
3.doReleaseShared():
a.循环操作:
i.检查头节点 h(因为共享模式下多个线程可能同时释放)。
ii.如果 h.waitStatus == SIGNAL,尝试 CAS 将其设为 0 并 unparkSuccessor(h)。
iii.如果 h.waitStatus == 0,尝试 CAS 将其设为 PROPAGATE (-3)(传播状态),确保后续释放事件能传播下去。
iv.共享模式的唤醒是传播性的:一次成功的释放可能唤醒多个后继线程(或者设置状态让后续释放自动传播),以实现多个线程同时获取资源。
二、通知机制 (ConditionObject- signal()方法)
AQS 的通知机制是通过其内部类 ConditionObject实现的,对应于 Condition接口的 signal()/ signalAll()方法。这并非直接唤醒线程获取锁,而是将等待在条件队列上的线程转移到锁的主等待队列中参与锁竞争。
1.1.等待 (await()):
a.当线程调用 condition.await()时:
i.创建新的 Node节点,waitStatus = CONDITION (-2)。
ii.将此节点添加到与该 Condition关联的单向条件等待队列。
iii.释放锁: 调用 fullyRelease(node)完全释放当前持有的锁(state清零)。
iv.阻塞: 调用 LockSupport.park(this)阻塞当前线程。
2.2.通知 (signal()):
a.当线程(持有锁)调用 condition.signal()时:
i.从条件队列转移: 找到该条件队列中的第一个有效节点(未被取消的节点)。
ii.如果存在(firstWaiter != null):
1.调用 doSignal(firstWaiter)。
2.核心操作 (transferForSignal(Node node)):
a.尝试使用 CAS 将该节点的 waitStatus从 CONDITION (-2)设置为 0。如果失败,说明节点已被取消,忽略。
b.加入主同步队列: 调用 enq(node)方法,将该节点安全地 添加到锁的主 CLH 同步队列的尾部。
c.设置状态: 将该节点原来的前驱节点的 waitStatus设置为 SIGNAL (-1)(如果前驱状态不是 SIGNAL或已经被取消,则在主队列中进行清理和设置)。
3.将原 firstWaiter指向条件队列的下一个节点。
iii.调用 signalAll()会遍历整个条件队列,将所有未取消的节点都执行上述 transferForSignal()操作,转移到主队列。
3.3.等待线程的后续流程:
a.被 signal()唤醒的线程,此时只是被转移到了主同步队列中,状态依然是被 park()阻塞着。
b.该线程需要等待两种情况才能被真正唤醒并执行:
i.锁可用时的正常唤醒:
1.当之前持有锁并调用 signal()的线程执行完毕,释放锁(调用 release())时,会触发 unparkSuccessor()。
2.如果这个被转移的节点在 release()时成为了头节点的有效后继节点,它有可能在此刻被唤醒。
ii.await()中的唤醒检查: 在被 LockSupport.park()阻塞后,线程可以被以下几种方式打断唤醒:
1.被其他线程调用此节点的 Thread.interrupt()。
2.伪唤醒 (spurious wakeup)。
3.接收到 LockSupport.unpark(thread)(这正是 release()中的唤醒操作)。
iii.当线程从 park()中被唤醒时,它会退出 await()方法内部的循环:
a.在 await()方法中尝试重新获取锁 (acquireQueued(node, savedState)):这会将其加入到锁的竞争队列中。
b.如果成功获取到锁,线程继续执行 await()调用之后的代码。
c.在此期间会检查中断状态并恢复中断标志。
通知机制 (signal()) 核心思想:
1.状态转换: 将节点从 CONDITION状态转为 SIGNAL(或 0)。
2.队列迁移: 不是直接唤醒等待线程,而是将其从条件等待队列移动到锁的主同步队列的尾部排队。
3.延迟唤醒:
a.被 signal()的线程并不会立即执行。
b.它必须先重新排队竞争获取锁(acquireQueued),只有当前线程释放锁且轮到它成为有效等待者时,才会被唤醒(通过 release()的 unparkSuccessor())获取锁并继续执行。
4.保证条件谓词正确性:
a.这种设计强制要求通知线程在调用 signal()时必须持有锁,确保了在条件谓词改变后(通知发生的原因)到将线程移回主队列的时间段内,没有任何其他线程能修改共享状态,避免了条件谓词的虚假观察。
b.等待线程在 await()返回前会重新获取锁,保证了它在离开 await()时再次持有锁并看到通知线程做出的状态修改
总结:
- 释放锁 (release()):
- 核心是释放资源状态 (state) 并 唤醒主同步队列中第一个有效的后继线程 (unparkSuccessor())。
- 关键在操作 state和精准唤醒。
- 通知 (signal()):
- 核心是将条件队列中的节点转移到主同步队列 (transferForSignal()+ enq()) 并设置其前驱状态。
- 它本身并不唤醒线程,实际唤醒延迟到该线程在主队列中轮到它获取锁时(由 release()触发)或在响应中断时。
- 这保证了条件谓词的安全性。
- 联系:
- signal()和 release()是解耦的。
- signal()负责转移线程等待位置,release()负责真正决定何时唤醒队列中的哪个线程来获取资源。
- 后者才是释放锁的核心动作。理解它们如何协作是掌握 AQS 同步器行为的关键。
锁中的CAS是什么?
CAS(Compare And Swap)是锁机制中实现“无锁化线程安全”的核心原子操作,本质是一条CPU指令。 它的工作原理可以用“检查-修改”的原子操作来理解,避免传统锁的阻塞开销。下面用通俗方式解析:
核心:操作包含三个操作数:内存位置(V)、预期原值(A)、新值(B)。
🔧 一、CAS原理:像超市存包柜的占位操作
想象超市存包柜的使用场景:
1.操作流程:
◦ 你选一个柜子,先查看标签状态(假设 空闲=0)。
◦ 发现状态为 0(空闲),放入书包后 立即把标签改为 1(占用)。
◦ 关键点:修改标签的过程必须一次性完成,中间不能被打断。
1.若多人同时操作:
◦ 甲和乙都看到1号柜标签为 0(空闲)。
◦ 甲抢先完成操作:查看(0)→ 改为1(写入),成功占用。
◦ 乙随后尝试相同操作:
▪ 查看当前值 → 发现已是 1(甲已修改)。
▪ 对比期望值 → 期望是 0(但实际是 1)→ 操作失败。
◦ 乙需重试其他柜子或等待。
⚙️ 二、技术实现:CPU级别的原子指令
在计算机中,CAS的操作由 CPU硬件直接保证原子性,无需加锁:
bool CAS(int* ptr, int expected, int newValue) {
if (*ptr == expected) { // 比较内存当前值
*ptr = newValue; // 相等则更新
return true;
}
return false;
}
• 参数说明:
◦ ptr:内存位置(如锁的状态变量地址)
◦ expected:期待该位置当前值
◦ newValue:要写入的新值
• 执行结果:
◦ 成功 → 返回 true,值已被更新。
◦ 失败 → 返回 false(其他线程已修改值)。
🔒 三、在锁中的应用(以非公平锁为例)
以 ReentrantLock 的加锁流程说明CAS的作用:
1.加锁时:
◦ 线程尝试用CAS修改锁的 state 变量:
▪ 期望值 0(未锁定)→ 目标值 1(锁定)
◦ 若CAS成功,说明抢到锁,线程进入临界区。
◦ 若失败,则加入等待队列(AQS队列)休眠。
1.伪代码流程:
void lock() {
if (CAS(state, 0, 1)) { // 尝试快速抢锁
setOwner(currentThread); // 成功则设置锁持有者
} else {
acquireQueued(); // 失败则排队等待
}
}
⚖️ 四、CAS vs 传统锁的优劣
对比维度 CAS(无锁) 传统锁(如synchronized)
性能 无阻塞,高并发下吞吐量高 线程阻塞唤醒开销大
资源消耗 仅占用CPU周期,无上下文切换 频繁切换线程消耗内存
适用场景 短任务(如计数器自增) 长任务或复杂操作
问题风险 自旋导致CPU空转(需控制次数)
ABA问题(可通过版本号解决) 死锁风险
✅ 优势案例:
用CAS实现线程安全的计数器 AtomicInteger:
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // CAS自增代替synchronized
⚠️ 五、CAS的局限性及解决方案
1.ABA问题(中途被改后又改回)
◦ 场景:
线程1读取值 A → 线程2修改值 A→B→A → 线程1再执行CAS时误判“未被修改”。
◦ 解决:追加版本号(如 AtomicStampedReference)。
1.长时间自旋消耗CPU
◦ 自旋超过一定次数(如10次)应放弃,改用阻塞锁。
1.只能控制单个变量
◦ 复杂操作需搭配其他机制(如队列、状态机)。
💎 总结
CAS是锁机制中的“轻量级抢占术”:
• 核心价值:用单条CPU原子指令避免阻塞,提升并发效率。
• 适用场景:共享变量的简单修改(自增、标志位切换、对象引用替换)。
• 开发提示:在 java.util.concurrent.atomic 包中优先使用 AtomicXXX 类(如 AtomicInteger),而非手动实现CAS逻辑。
记住:CAS是构建高效锁(如 ReentrantLock)、信号量等同步工具的基础砖石。