AQS底层重构与性能提升
AQS底层重构与性能提升
- 前言
- JDK9中AQS底层重构与性能提升
- 一、 演进背景:从 `Unsafe` 到 `VarHandle` 的必然选择
- 1. JDK 8 及以前 `Unsafe` 的痛点
- 2. JDK 9+ `VarHandle` 的设计优势
- 二、 VarHandle 的四种核心内存访问模式
- 三、 AQS 源码实现演进:JDK 8 (Unsafe) vs JDK 9+ (VarHandle)
- 1. 变量句柄初始化与偏移量计算对比
- JDK 8 (`Unsafe` 时代)
- JDK 9+ (`VarHandle` 时代)
- 2. CAS 与内存写操作的直观代码对比
- JDK 8 (Unsafe) 的 CAS 与 Put:
- JDK 9+ (VarHandle) 的 CAS 与 Put:
- 四、 内存可见性与指令重排序上的深度改进
- 1. 精细化内存屏障:Acquire-Release 语义的完美契合
- CLH 队尾入队场景
- 建立的 Happens-Before 关系:
- 2. 弱内存模型 CPU 架构(如 ARM64)下的指令级优化红利
- x86/x64 (TSO 强内存模型) 的局限性掩盖
- ARM64 / PowerPC (弱内存模型) 的硬件指令匹配
- 3. Opaque(不透明)访问模式对编译器重排序的精准约束
- 编译器的“寄存器提升”风险(Loop Invariant Hoisting)
- Opaque 模式的解决之道
- 4. CAS 操作族的语义扩展(CompareAndExchange 与 Weak CAS)
- ① `compareAndExchangeAcquire` / `compareAndExchangeRelease`
- ② 精确控制屏障级别的 `weakCompareAndSet`
- 五、 总结:AQS 底层重构的核心价值
前言
本文旨在记录近期研读Java源码的学习心得与疑难问题。由于个人理解水平有限,文中内容难免存在疏漏,恳请读者不吝指正。
JDK9中AQS底层重构与性能提升
在 JDK 9 中,Java 引入了JEP 193: Variable Handles (VarHandle),并在java.util.concurrent包中进行了大规模重构,其中最核心的变动之一就是将AbstractQueuedSynchronizer(AQS) 底层依赖的sun.misc.Unsafe彻底替换为java.lang.invoke.VarHandle。
这一转变不仅解决了 JVM 强封装性与安全性问题,更在内存访问语义的精细化控制、指令重排序约束以及非 x86 弱内存模型 CPU 架构(如 ARM64)下的执行效率上带来了质的飞跃。
一、 演进背景:从Unsafe到VarHandle的必然选择
1. JDK 8 及以前Unsafe的痛点
在 JDK 8 中,AQS 通过直接调用sun.misc.Unsafe的私有 API 来完成基于内存偏移量(objectFieldOffset)的 CAS 和内存屏障操作:
- 破坏强封装性与类型安全:
Unsafe绕过了 Java 类型检查,直接操作裸内存。如果偏移量计算错误,将导致 JVM 崩溃或不可预知的数据损坏。 - 内存访问模式过于粗暴:
Unsafe仅支持极其有限的内存操作模式(主要是普通读写、volatile读写,以及基于 StoreStore 屏障的putOrdered),缺乏类似于 C++11 内存模型中丰富且精准的内存语义控制。 - JDK 模块化(Project Jigsaw)的阻碍:
sun.misc.Unsafe属于 JDK 内部私有 API,从 JDK 9 开始引入模块系统后,内部 API 被限制强行对外暴露。
2. JDK 9+VarHandle的设计优势
java.lang.invoke.VarHandle是对变量引用的一层高抽象、类型安全且高性能的句柄。它将变量的强类型访问与 C++11 风格的内存访问模式(Memory Access Modes)相结合,提供了强类型的 CAS、原子自增、Acquire/Release 语义以及 Opaque(不透明)读写能力。
二、 VarHandle 的四种核心内存访问模式
VarHandle 为变量读写定义了四种由弱到强的内存语义控制模式,这是替换Unsafe后的核心能力基础:
| 内存访问模式 | 代表方法 | 内存屏障 / 重排序约束 | 适用场景 |
|---|---|---|---|
| Plain (普通) | get(),set() | 无屏障。允许编译器与 CPU 进行最大程度的重排序。 | 线程内局部访问或受其他锁保护的变量。 |
| Opaque (不透明) | getOpaque(),setOpaque() | 无硬件级内存屏障。仅保证操作的原子性(如 double/long 的 64 位写)与程序执行顺序(Program Order),阻止编译器将读写操作跨循环优化或寄存器提升。 | 仅需要线程间进度可见,无需 happens-before 规则约束其他变量的场景。 |
| Acquire / Release (获取/释放) | getAcquire(),setRelease() | 半屏障(Half Barrier)。 |
•Acquire Read:防止其后的读写指令重排序到该读操作之前。
•Release Write:防止其前的读写指令重排序到该写操作之后。 | 构建单向依赖的happens-before关系,无全屏障开销。 |
|Volatile (顺序一致性)|getVolatile(),setVolatile()|全屏障(Full Fence)。完全禁止屏障前后的指令跨越重排序,满足全局顺序一致性(Sequential Consistency)。 | 强可见性要求,如竞争极其剧烈的状态变更。 |
三、 AQS 源码实现演进:JDK 8 (Unsafe) vs JDK 9+ (VarHandle)
1. 变量句柄初始化与偏移量计算对比
JDK 8 (Unsafe时代)
在 JDK 8 中,AQS 在静态代码块中显式获取Unsafe实例,并通过反射计算各个属性在对象内存中的字节偏移量:
// JDK 8 AbstractQueuedSynchronizer.javapublicabstractclassAbstractQueuedSynchronizer{privatestaticfinalUnsafeunsafe=Unsafe.getUnsafe();privatestaticfinallongstateOffset;privatestaticfinallongheadOffset;privatestaticfinallongtailOffset;privatestaticfinallongwaitStatusOffset;privatestaticfinallongnextOffset;static{try{stateOffset=unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("state"));headOffset=unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("head"));tailOffset=unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("tail"));waitStatusOffset=unsafe.objectFieldOffset(Node.class.getDeclaredField("waitStatus"));nextOffset=unsafe.objectFieldOffset(Node.class.getDeclaredField("next"));}catch(Exceptionex){thrownewError(ex);}}}JDK 9+ (VarHandle时代)
在 JDK 9 及更高版本中,AQS 改用MethodHandles.Lookup安全地查找字段句柄,不再依赖内存字节偏移量:
// JDK 9+ AbstractQueuedSynchronizer.javapublicabstractclassAbstractQueuedSynchronizer{// 声明强类型的 VarHandle 句柄privatestaticfinalVarHandleSTATE;privatestaticfinalVarHandleHEAD;privatestaticfinalVarHandleTAIL;static{try{MethodHandles.Lookupl=MethodHandles.lookup();STATE=l.findVarHandle(AbstractQueuedSynchronizer.class,"state",int.class);HEAD=l.findVarHandle(AbstractQueuedSynchronizer.class,"head",Node.class);TAIL=l.findVarHandle(AbstractQueuedSynchronizer.class,"tail",Node.class);}catch(ReflectiveOperationExceptione){thrownewExceptionInInitializerError(e);}}}2. CAS 与内存写操作的直观代码对比
JDK 8 (Unsafe) 的 CAS 与 Put:
// 更改 stateprotectedfinalbooleancompareAndSetState(intexpect,intupdate){returnunsafe.compareAndSwapInt(this,stateOffset,expect,update);}// 惰性设置 next (使用了 StoreStore 屏障,等价于 Release Write)node.waitStatus=Node.SIGNAL;unsafe.putOrderedObject(node,nextOffset,nextNode);JDK 9+ (VarHandle) 的 CAS 与 Put:
// 默认 compareAndSet 具有 Volatile (顺序一致性) 语义protectedfinalbooleancompareAndSetState(intexpect,intupdate){returnSTATE.compareAndSet(this,expect,update);}// 精准使用 setRelease 替代原先语义含糊的 putOrderedObjectNEXT.setRelease(node,nextNode);四、 内存可见性与指令重排序上的深度改进
替换为 VarHandle 绝非仅仅是 API 的升级,它在底层 CPU 指令生成、编译器优化限制以及并发控制精度上带来了深刻的变革。
1. 精细化内存屏障:Acquire-Release 语义的完美契合
在 CLH 队列的出队与入队流程中,并不是所有地方都需要代价高昂的Full Memory Barrier(全内存屏障)。
CLH 队尾入队场景
当新节点入队时,执行tail指针的更新与前驱节点next指针的挂载:
- JDK 8 实现:使用
unsafe.putOrderedObject,其底层对应的是 JMM 的StoreStore屏障。虽然避免了StoreLoad屏障,但其语义并没有明确与“读取”动作建立同步绑定。 - JDK 9+ 实现:
// Node 入队逻辑node.setPrevRelaxed(oldTail);// 使用 Plain/Opaque 模式写 prev,因为此时该节点尚未对其他线程可见if(TAIL.compareAndSet(this,oldTail,node)){NEXT.setRelease(oldTail,node);// 关键:使用 Release 语义写前驱节点的 next}在另外一边,自旋检查或唤醒后继线程时,读取next节点使用getAcquire():
Nodes=node.next;if(s==null||s.status>0){// 使用 Acquire 语义读取 next 节点s=(Node)NEXT.getAcquire(node);}建立的 Happens-Before 关系:
通过NEXT.setRelease与NEXT.getAcquire配对,JVM 在逻辑上建立了Release-Acquire Ordering:
NEXT.setRelease之前的所有内存写操作(如node的初始化、prev指针设置),对于执行NEXT.getAcquire成功读到该node的线程均完全可见。
这种单向屏障(Half-Barrier)避免了全屏障在流水线上的停顿,显著提高了 CLH 链表并发构建的吞吐量。
2. 弱内存模型 CPU 架构(如 ARM64)下的指令级优化红利
这是 JDK 9 引入 VarHandle 后带来的最大硬件级性能收益。
x86/x64 (TSO 强内存模型) 的局限性掩盖
在 x86 架构下,硬件本身保证了 Store-Store、Load-Load、Load-Store 的顺序,只有 Store-Load 会被重排序。因此在 x86 上:
volatile读与普通读生成的指令完全一样(均为普通mov)。volatile写只需追加一个lock addl或mfence指令。
这导致在 x86 平台上,Unsafe粗糙的内存屏障带来的额外性能损失并不明显。
ARM64 / PowerPC (弱内存模型) 的硬件指令匹配
在 ARM64 等弱内存模型 CPU 上,读与写均可能被任意重排序,硬件层面提供了专门的单向屏障指令:
LDAR(Load-Acquire Register):原子的 Acquire 读指令。STLR(Store-Release Register):原子的 Release 写指令。
在JDK 8 (Unsafe)中,由于缺乏 Acquire/Release 模式,JVM 面对volatile读写只能保守地插入重型内存屏障指令DMB ISH(Data Memory Barrier),这会导致 CPU 流水线彻底刷新(Flush Pipeline),开销极大。
在JDK 9+ (VarHandle)中:
NEXT.getAcquire()被 HotSpot JIT 直接编译为 ARM64 的LDAR指令。NEXT.setRelease()被直接编译为 ARM64 的STLR指令。
LDAR/STLR是硬件级别的微架构优化指令,其执行延迟远低于DMB屏障。因此,AQS 在 JDK 9 下在 ARM64 服务器(如 AWS Graviton、鲲鹏等)上的并发性能得到了成倍提升。
3. Opaque(不透明)访问模式对编译器重排序的精准约束
在 AQS 的死循环(for(;;)自旋抢锁)中,某些变量需要频繁读取,但并不需要每次都触发硬件屏障。
编译器的“寄存器提升”风险(Loop Invariant Hoisting)
如果使用纯粹的普通读取(Plain Access)在循环体内读取一个非volatile变量,JIT 编译器可能会认为该变量在循环体内没有改变,从而将其优化提升(Hoist)到寄存器中,导致循环永远无法感知到其他线程修改了该变量:
// 假设是 Plain 读,JIT 可能将其优化为:intstatus=node.waitStatus;while(status<0){// JIT 不会每次都去主内存读 status,死循环!}Opaque 模式的解决之道
VarHandle 提供了getOpaque()和setOpaque():
// AQS 内部 Node 属性读取intws=(int)WAITSTATUS.getOpaque(node);- 对 JIT 编译器的约束:禁止 JIT 将该读取操作跨循环优化,强制每一次循环必须重新产生从内存(或 L1/L2 Cache)读取的 Load 指令。
- 对 CPU 硬件的约束:不产生任何硬件级内存屏障指令(无 Fence),CPU 依然可以自由重排序非依赖指令。
Opaque 模式以零硬件开销的代价,精准解决了“编译器重排序与死循环”问题。
4. CAS 操作族的语义扩展(CompareAndExchange 与 Weak CAS)
VarHandle 为 AQS 带来了更丰富的原子指令能力,主要体现在compareAndExchange和weakCompareAndSet上:
①compareAndExchangeAcquire/compareAndExchangeRelease
传统的compareAndSet仅返回boolean(成功/失败)。如果失败,调用者通常需要重新发起一次get()读取才能知道当前最新的值是什么。
JDK 9+ 的 VarHandle 引入了类似 C++11 的compareAndExchange:
// 若期望值匹配,更新为 newStatus 并返回旧值;若不匹配,直接返回内存中的实际当前值intwitness=(int)WAITSTATUS.compareAndExchangeAcquire(node,Node.WAITING,Node.CANCELLED);if(witness!=Node.WAITING){// 直接拿到 witness 进行逻辑判断,无需再次调用 get() 重新读取!}这避免了失败分支下额外的一次内存读取指令。
② 精确控制屏障级别的weakCompareAndSet
Unsafe时代的weakCompareAndSet在许多 JVM 实现中直接退化为了普通的强 CAS。而在 VarHandle 中,weakCompareAndSetPlain/weakCompareAndSetAcquire被赋予了明确的语义:
- 允许虚假失败(Spurious Failure)。
- 不保证强顺序一致性。
- 编译为支持 Load-Linked / Store-Conditional (
LL/SC) 的 CPU 指令(如 ARM 上的LDREX/STREX),在自旋锁场景下比强 CAS 性能更高。
五、 总结:AQS 底层重构的核心价值
AQS 从Unsafe迁移至VarHandle,是 Java 并发工具包向现代 C++11/C11 内存模型看齐的里程碑式改进:
[ Unsafe 时代 (JDK 8) ] Unsafe (内存偏移量 + 物理地址直写) ├── 只有 Plain / Volatile / putOrdered 三种粗粒度模式 └── 依赖重型 DMB/MFENCE 全屏障,ARM64 等弱内存模型架构下开销巨大 ▼ 演进与重构 (JDK 9+) [ VarHandle 时代 (JDK 9+) ] VarHandle (强类型句柄 + 现代 C++11 内存模型) ├── Plain : 无屏障,极尽编译器优化 ├── Opaque : 无硬件屏障,防止 JIT 循环寄存器提升 ├── Acquire/Release : 单向半屏障,匹配 ARM64 的 LDAR/STLR 硬件指令 └── Volatile: 全屏障,保证绝对顺序一致性- 安全性与解耦:彻底摒弃裸指针与内存偏移量,全面回归 Java 的类型安全与 JVM 强封装机制。
- 指令级性能优化:通过
Acquire / Release模式精准映射 ARM64 等现代化 CPU 的LDAR/STLR硬件指令,在非 x86 服务器上获得了巨大的并发性能提升。 - 内存控制粒度解耦:允许 AQS 开发者根据 CLH 队列不同阶段的线程可见性需求,精细化选择
Opaque、Acquire/Release或Volatile,在保障并发可见性与发挥 CPU 指令乱序并行能力之间取得了极佳的平衡。
