AQS原理
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))
- tryAcquire(arg): 子类必须实现此模板方法!根据自定义策略尝试直接获取资源(操作
state)。成功返回true,线程继续执行。 - 失败入队: 如果
tryAcquire失败(返回false):- 将当前线程包装成一个
Node节点。 - CAS入队尾: 使用
compareAndSetTail以无锁(乐观锁)方式将该节点安全地添加到队列尾部。 acquireQueued(final Node node, int arg): 进入此方法,核心在自旋循环中:- 检查前驱: 检查新加入节点(或其有效前置节点)的前一个节点
p是否是head(队列中第一个等待线程)。 - 尝试获取: 如果是
head,则调用tryAcquire(arg)再次尝试获取资源(给刚入队的线程一次机会)。 - 获取成功: 将当前节点设置为新的
head(原head出队),返回中断标记。 - 获取失败或被唤醒后检查:
- 调用
shouldParkAfterFailedAcquire(Node pred, Node node):检查前置节点状态。- 如果
pred.waitStatus == SIGNAL(表示pred有义务在释放时唤醒后续节点),则当前线程可以安全阻塞park。 - 否则(如CANCELLED状态),跳过无效的前驱节点直到找到有效前驱,并将其
waitStatus设置为SIGNAL。
- 如果
- 调用
parkAndCheckInterrupt():使用LockSupport.park(this)阻塞当前线程。线程在此处挂起。
- 调用
- 线程唤醒: 被前驱节点释放资源时唤醒或响应中断唤醒。
- 检查中断: 判断唤醒是否为中断引起,并返回中断状态。
- 检查前驱: 检查新加入节点(或其有效前置节点)的前一个节点
- 处理中断: 如果在等待过程中被中断,
acquire通常会将中断再标记(selfInterrupt),以符合Lock API的约定(不响应中断,但保留标记)。
- 将当前线程包装成一个
释放资源 (release(int arg))
- tryRelease(arg): 子类必须实现此模板方法!尝试释放资源(操作
state)。成功返回true。 - 唤醒后继: 如果
tryRelease成功:- 检查头节点
h(通常是持有资源的节点)。 - 如果
h.waitStatus != 0(通常为SIGNAL,表示后继节点在等待唤醒):- 调用
unparkSuccessor(Node node):- 尝试将节点的
waitStatus重置为0。 - 找到队列中第一个未取消的有效后继节点
s(从尾向头找最前面的有效节点是一种优化)。 - 如果
s不为null,使用LockSupport.unpark(s.thread)唤醒该后继节点的线程。该线程在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() 方法)
这是独占模式下释放资源的流程:
-
tryRelease(int arg)尝试释放资源:- 由子类具体实现(如
ReentrantLock.Sync)。 - 核心: 操作
state变量。通常是递减或重置state。- 在
ReentrantLock中:state = state - releases(可重入锁需要多次释放直到state == 0才算完全释放)。
- 在
- 返回值: 如果操作后资源已被完全释放(
state == 0),则返回true。只有当返回true时,AQS 才会触发唤醒后继线程。对于可重入锁,只有最后一次释放才会返回true。
- 由子类具体实现(如
-
检查并唤醒后继节点 (
unparkSuccessor(Node node)):- 如果
tryRelease返回true,说明资源完全释放,需要唤醒队列中合适的等待线程。 - 获取当前头节点
h(持有资源的节点正在释放资源)。 - 检查头节点的
waitStatus:- 如果
h.waitStatus < 0(通常是SIGNAL (-1)或PROPAGATE (-3)):- 重置状态: 使用 CAS 将头节点的
waitStatus设为 0 (减少不必要的唤醒信号)。
- 重置状态: 使用 CAS 将头节点的
- 如果
h.waitStatus >= 0(可能是0或CANCELLED (1)),说明没有有效后继节点需要唤醒,直接跳过。
- 如果
- 寻找有效后继节点:
- 首先尝试:从
h.next(后继)开始向后查找。 - 关键优化: 如果
s == null || s.waitStatus > 0(表示后继节点无效或被取消):- 从队尾向前遍历 (
tail→head) 找到队列中waitStatus <= 0的最前面的有效节点。 - 为什么从尾向前? 因为节点入队是“设置新节点的
prev = oldTail” + “CAS更新tail = 新节点”两步操作。node.next = nextNode的链接是在之后设置(或由唤醒线程设置)。如果在并发场景下从前往后找,可能next指针还未正确设置。从后往前利用已经稳定的prev指针则能保证总能找到完整的队列。
- 从队尾向前遍历 (
- 首先尝试:从
- 唤醒线程: 找到有效后继节点
s后,调用LockSupport.unpark(s.thread)唤醒该节点关联的线程。- 该线程从之前在
acquireQueued()中的LockSupport.park()处被唤醒。 - 被唤醒的线程会重新在
acquireQueued()的自旋循环中尝试获取资源 (tryAcquire())。由于资源已被释放,它有很大概率(在非公平锁下还需竞争)能成功获取锁,成为新的head节点并继续执行。
- 该线程从之前在
- 如果
释放锁机制核心思想:
- 状态更新在前: 先通过
tryRelease安全释放资源(更新state)。 - 精准唤醒: 一旦确认资源可被获取,按照 FIFO 原则 唤醒队列中 第一个有效且未被取消 的等待线程(头节点的后继)。
- 低开销唤醒: 使用
unpark()精确唤醒一个线程,避免了不必要的线程上下文切换开销。 - 竞争重新开始: 被唤醒的线程需要重新尝试获取锁,这保证了 非公平锁的“插队”特性(即使有新线程此时来抢锁,也可能成功)。
共享模式 (releaseShared()):
tryReleaseShared(int arg): 子类实现释放资源逻辑(如Semaphore中是state = state + releases),可能需要循环 CAS。- 如果
tryReleaseShared返回true(表示资源释放成功且状态变化可能允许其他线程获取),调用doReleaseShared()。 doReleaseShared():- 循环操作:检查头节点
h(因为共享模式下多个线程可能同时释放)。 - 如果
h.waitStatus == SIGNAL,尝试 CAS 将其设为 0 并unparkSuccessor(h)。 - 如果
h.waitStatus == 0,尝试 CAS 将其设为PROPAGATE (-3)(传播状态),确保后续释放事件能传播下去。 - 共享模式的唤醒是传播性的:一次成功的释放可能唤醒多个后继线程(或者设置状态让后续释放自动传播),以实现多个线程同时获取资源。
- 循环操作:检查头节点
二、通知机制 (ConditionObject - signal() 方法)
AQS 的通知机制是通过其内部类 ConditionObject 实现的,对应于 Condition 接口的 signal() / signalAll() 方法。这并非直接唤醒线程获取锁,而是将等待在条件队列上的线程转移到锁的主等待队列中参与锁竞争。
-
等待 (
await()):- 当线程调用
condition.await()时:- 创建新的
Node节点,waitStatus = CONDITION (-2)。 - 将此节点添加到与该
Condition关联的单向条件等待队列。 - 释放锁: 调用
fullyRelease(node)完全释放当前持有的锁(state清零)。 - 阻塞: 调用
LockSupport.park(this)阻塞当前线程。
- 创建新的
- 当线程调用
-
通知 (
signal()):- 当线程(持有锁)调用
condition.signal()时:- 从条件队列转移: 找到该条件队列中的第一个有效节点(未被取消的节点)。如果存在(
firstWaiter != null):- 调用
doSignal(firstWaiter)。 - 核心操作 (
transferForSignal(Node node)):- 尝试使用 CAS 将该节点的
waitStatus从CONDITION (-2)设置为0。如果失败,说明节点已被取消,忽略。 - 加入主同步队列: 调用
enq(node)方法,将该节点安全地 添加到锁的主 CLH 同步队列的尾部。 - 设置状态: 将该节点原来的前驱节点的
waitStatus设置为SIGNAL (-1)(如果前驱状态不是SIGNAL或已经被取消,则在主队列中进行清理和设置)。
- 尝试使用 CAS 将该节点的
- 调用
- 将原
firstWaiter指向条件队列的下一个节点。
- 从条件队列转移: 找到该条件队列中的第一个有效节点(未被取消的节点)。如果存在(
- 调用
signalAll()会遍历整个条件队列,将所有未取消的节点都执行上述transferForSignal()操作,转移到主队列。
- 当线程(持有锁)调用
-
等待线程的后续流程:
- 被
signal()唤醒的线程,此时只是被转移到了主同步队列中,状态依然是被park()阻塞着。 - 该线程需要等待两种情况才能被真正唤醒并执行:
- 锁可用时的正常唤醒: 当之前持有锁并调用
signal()的线程执行完毕,释放锁(调用release())时,会触发unparkSuccessor()。如果这个被转移的节点在release()时成为了头节点的有效后继节点,它有可能在此刻被唤醒。 await()中的唤醒检查: 在被LockSupport.park()阻塞后,线程可以被以下几种方式打断唤醒:- 被其他线程调用此节点的
Thread.interrupt()。 - 伪唤醒 (spurious wakeup)。
- 接收到
LockSupport.unpark(thread)(这正是release()中的唤醒操作)。
- 被其他线程调用此节点的
- 当线程从
park()中被唤醒时,它会退出await()方法内部的循环:- 在
await()方法中尝试重新获取锁 (acquireQueued(node, savedState)):这会将其加入到锁的竞争队列中。 - 如果成功获取到锁,线程继续执行
await()调用之后的代码。 - 在此期间会检查中断状态并恢复中断标志。
- 在
- 锁可用时的正常唤醒: 当之前持有锁并调用
- 被
通知机制 (signal()) 核心思想:
- 状态转换: 将节点从
CONDITION状态转为SIGNAL(或 0)。 - 队列迁移: 不是直接唤醒等待线程,而是将其从条件等待队列移动到锁的主同步队列的尾部排队。
- 延迟唤醒: 被
signal()的线程并不会立即执行。它必须先重新排队竞争获取锁(acquireQueued),只有当前线程释放锁且轮到它成为有效等待者时,才会被唤醒(通过release()的unparkSuccessor())获取锁并继续执行。 - 保证条件谓词正确性: 这种设计强制要求通知线程在调用
signal()时必须持有锁,确保了在条件谓词改变后(通知发生的原因)到将线程移回主队列的时间段内,没有任何其他线程能修改共享状态,避免了条件谓词的虚假观察。等待线程在await()返回前会重新获取锁,保证了它在离开await()时再次持有锁并看到通知线程做出的状态修改。
总结:
- 释放锁 (
release()): 核心是释放资源状态 (state) 并 唤醒主同步队列中第一个有效的后继线程 (unparkSuccessor())。关键在操作state和精准唤醒。 - 通知 (
signal()): 核心是将条件队列中的节点转移到主同步队列 (transferForSignal()+enq()) 并设置其前驱状态。它本身并不唤醒线程,实际唤醒延迟到该线程在主队列中轮到它获取锁时(由release()触发)或在响应中断时。这保证了条件谓词的安全性。 - 联系:
signal()和release()是解耦的。signal()负责转移线程等待位置,release()负责真正决定何时唤醒队列中的哪个线程来获取资源。后者才是释放锁的核心动作。理解它们如何协作是掌握 AQS 同步器行为的关键。
CAS抢占式机制
CAS(Compare-And-Swap)是并发编程中一种无锁(lock-free) 的原子操作机制,被誉为“抢占式”机制的核心,尤其是在Java AQS、原子类等并发控制中广泛使用。它直接在硬件层面(CPU指令)实现,提供了极高的性能。
核心原理
CAS操作包含三个参数:
- 内存地址(V): 需要读写的内存位置(通常是共享变量的引用)。
- 预期原值(A): 线程认为该内存位置当前应该具有的值(基于本地副本)。
- 新值(B): 希望将该内存位置更新为的值。
操作语义(原子性保障):
function CAS(V, A, B) {
if (V == A) { // 比较:检查内存位置V的实际值是否与预期值A相等
V = B; // 交换:如果相等,则将该内存位置的值更新为新值B
return true; // 操作成功
} else {
return false; // 操作失败
}
}
关键点: 整个“比较”和“交换”动作是由一条CPU指令(如x86的CMPXCHG)原子完成的。在执行该指令期间,不会被其他线程打断,因此保证了操作的线程安全性。
为何称为“抢占式”机制?
CAS的“抢占”特性体现在它的竞争处理方式上:
- “乐观”尝试: 线程在修改共享变量前,并不先加锁阻塞其他线程。它只是乐观地读取当前值(作为A),在本地计算出希望设置的新值(B)。
- “抢占式”更新: 当线程执行CAS(V, A, B)时,它尝试在瞬间“抢占”内存位置V的控制权:
- 如果此刻V的值恰好等于A(说明该线程读取后未被其他线程修改过),则CAS“抢修”成功,立即将V更新为B。
- 如果此刻V的值不等于A(说明该线程读取值之后,V已经被其他线程修改过),则CAS“抢占”失败。不会进行任何更新。
- 失败后的处理策略(轮询/重试):
- 自旋(Spin): 常见的策略是立即重试(忙等)。线程再次读取V的当前最新值作为新的预期值A_new,重新计算出新值B_new,然后再次尝试CAS(V, A_new, B_new)。这就像反复尝试“抢答”一样。
- 放弃/重试: 也可以放弃操作或执行特定失败逻辑。在AQS中,例如节点入队失败,可能会重新循环尝试入队。
优点(对比传统锁)
- 无阻塞(Non-Blocking): 避免了线程因获取锁失败导致的阻塞(挂起)和上下文切换开销。对于竞争不激烈或操作耗时很短的情况,性能显著优于锁。
- 高并发: 多个线程可以同时尝试更新内存,只有预期值与内存值匹配的线程能更新成功。
- 避免死锁: 由于线程不会被挂起等待锁,避免了死锁的核心必要条件之一(请求与保持条件)。
- 简单性: 对于简单的原子更新操作,用CAS表达逻辑相对清晰(虽然失败重试逻辑需要小心处理)。
缺点与挑战
- ABA问题:
- 场景: 线程1读取变量值为A。随后,线程2先把值修改为B,然后又修改回A!此时,线程1执行CAS(V, A, B),发现当前值确实是A,于是CAS成功。但事实上变量的历史状态发生了变化(A->B->A)。
- 危害: 对于依赖状态连续性的场景(如链表操作),这可能导致逻辑错误(例如,误以为头节点没变,实际上中间已经被删掉后又重建了)。
- 解决方案:
- 版本号/时间戳(推荐): 每次修改都增加一个版本号。比较时同时比较值和版本号。Java的
AtomicStampedReference、AtomicMarkableReference就是为此设计的。 - 互斥锁: 在极端情况下引入锁。
- 版本号/时间戳(推荐): 每次修改都增加一个版本号。比较时同时比较值和版本号。Java的
- 自旋带来的CPU开销:
- 在高竞争(很多线程频繁修改V) 的场景下,大量线程会反复进行失败的CAS尝试和重试(自旋),导致CPU空转,消耗大量资源,性能反而比使用锁更差。
- 优化策略:
- 自适应自旋: JVM动态调整自旋次数(如自旋一小段时间后还没成功则挂起)。
- 退避策略: 在重试之间增加短暂延迟(如指数退避)。
- 减少竞争范围: 设计更好的数据结构或算法减少冲突。
- 只能保证一个变量的原子性:
- 一次CAS操作只能原子性地更新一个内存位置(变量)。如果需要原子性地操作多个变量,就会变得复杂(需要用锁或其他机制保证整体原子性)。
- 复杂性:
- 构建复杂的无锁数据结构(如队列、栈)比使用锁的版本逻辑更复杂,代码调试和维护难度更高。
CAS在Java中的具体应用与使用
java.util.concurrent.atomic包:- 核心类:
AtomicInteger,AtomicLong,AtomicBoolean,AtomicReference,AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray,AtomicStampedReference,AtomicMarkableReference等。 - 核心方法:
compareAndSet(expectedValue, newValue)(最常用),getAndSet(newValue),incrementAndGet(),getAndIncrement(),addAndGet(delta),getAndAdd(delta)等。这些方法底层几乎都是用Unsafe类的CAS操作(或类似语义)实现的。
- 核心类:
- AQS(核心中的核心):
- 状态更新 (
state):compareAndSetState(int expect, int update)方法尝试原子性地将AQS的state从预期值expect更新为update。这是获取锁(acquire)和释放锁(release)逻辑中操作状态的核心。 - 等待队列管理:
compareAndSetHead(Node update):用于初始化队列头(或设置新头节点)。compareAndSetTail(Node expect, Node update):用于将新节点原子地加入到队列尾部。compareAndSetWaitStatus(Node node, int expect, int update):用于修改队列中节点(主要是前驱节点)的等待状态(如设置SIGNAL)。
- 这些CAS操作是实现AQS非阻塞/低阻塞算法的基石。当这些CAS失败时,AQS通常会在循环中重试(体现“抢占式”重试)。
- 状态更新 (
总结
CAS是一种 “乐观锁”/“非阻塞同步”/“无锁编程” 的核心原子操作。它通过硬件支持的原子“比较并交换”指令,允许线程在不使用传统锁的情况下安全地更新共享变量。
它的“抢占式”本质体现在:
- 乐观尝试: 线程基于本地副本计算新值。
- 原子性竞争: 通过单指令尝试瞬间“抢占”更新权。
- 忙等重试: 失败时通过循环(自旋)进行重试。
虽然存在ABA问题、自旋开销和高竞争下性能劣化等挑战,但在中低竞争场景和特定数据结构(如AQS队列、原子计数器)中,CAS带来的性能优势是巨大的,是构建高效并发组件(如AQS、并发集合)不可或缺的技术。

170万+

被折叠的 条评论
为什么被折叠?



