Skip to content
Go back

Unsafe类——JUC所有无锁结构的原子操作基石

Unsafe:JUC 最底层的”危险”工具

一句话结论(30s)

Unsafe 是 JUC 所有无锁并发结构的「原子操作基石」,本质是绕过 Java 安全检查、直达 JVM 和 CPU 底层的一把「危险」工具。它的关键能力是三类:CAS(compareAndSwapInt 最终映射为 LOCK CMPXCHG 单条指令)、线程 park/unpark(LockSupport 的底层)、以及堆外内存 + 字段偏移访问。核心权衡是「性能 + 能力」换「安全」——因为 putObjectVolatile 直接按「对象指针 + 偏移」算地址写入、没有类型检查,堆外内存也不受 GC 管理,所以 JDK 9 起用类型安全的 VarHandle 作为推荐替代。

核心原理(2min)

主流程:AtomicInteger 的 getAndIncrement 最终调 Unsafe.compareAndSwapInt,由 LOCK 前缀锁定总线/缓存行保证多核原子性,CMPXCHG 单条指令完成「比较 + 交换」;先想一步:为什么一条 CMPXCHG 就能原子?因为 LOCK 前缀会把「读-改-写」锁定在总线/缓存行上,别的核插不进手。park/unpark 相比 wait/notify 不需要持 Monitor、unpark 可在 park 前调用(许可累积)、只唤醒指定线程不惊群;内存操作里 allocateMemory/freeMemory 管理堆外内存(必须手动 free 否则泄漏),objectFieldOffset + putObjectVolatile 用偏移直接做 volatile 写、比反射快得多。关键替代品是 VarHandle——提供 Plain/Opaque/Release-Acquire/Volatile 四种访问语义且类型安全。

底层深入(5-10min)

Unsafe 的三类能力

1. CAS 原子操作

AtomicInteger.getAndIncrement() 最终会落到这条 native 声明上:

// jdk.internal.misc.Unsafe(JDK 源码逐字)
@IntrinsicCandidate
public final native boolean compareAndSetInt(Object o, long offset,
                                             int expected,
                                             int x);

分析:这是 JUC 所有原子类 CAS 的最终落点,@IntrinsicCandidate 表示 JIT 会把它内联替换为 CPU 级指令。o 是目标对象、offset 是字段偏移、expected/x 分别是「当前值/新值」,返回布尔表示替换是否成功。

在 x86 上它最终映射为一条 CPU 指令:

LOCK CMPXCHG [addr], newVal  // 原子比较并交换

LOCK 前缀锁定总线/缓存行,保证多核同时 CAS 同一地址的原子性。CMPXCHG 是 x86 的物理实现——单条指令完成”比较+交换”的不可分割操作。(注:旧版 compareAndSwapInt 已废弃,新 JDK 统一为 compareAndSetInt。)这里可以再问一句:CAS 失败时调用方怎么办?——它只会返回 false,重试、自旋、退化成锁,全都交给上层决定,Unsafe 只负责「试着换一次」,这正是无锁编程里「乐观重试」的起点。

2. 线程 park/unpark

LockSupport.park()/unpark() 的底层就是这两条 native 声明:

// jdk.internal.misc.Unsafe(JDK 源码逐字)
@IntrinsicCandidate
public native void unpark(Object thread);

@IntrinsicCandidate
public native void park(boolean isAbsolute, long time);

分析:unpark 直接传目标线程对象,只唤醒指定线程(不惊群);park 的第一个参数区分「相对超时(纳秒)」还是「绝对截止时间(毫秒)」,配合 unpark 的「许可」语义——unpark 可在 park 之前调用,许可会累积到下一次 park 直接返回。相比 Object.wait/notify:不需要持 Monitor、unpark 可提前调用、只唤醒指定线程不惊群。想一想为什么 AQS、CountDownLatch 这些 JUC 同步器底层几乎全用 park/unpark?因为锁的「等待-唤醒」要频繁、精准、无锁——wait/notify 必须先持 Monitor 才能用,且 notify 会随机惊群,而 park/unpark 的许可语义更贴合同步器内部的状态机。

3. 内存操作

字段的 volatile 访问同样是 native 声明:

// jdk.internal.misc.Unsafe(JDK 源码逐字,字段 volatile 读)
@IntrinsicCandidate
public native Object getReferenceVolatile(Object o, long offset);

分析:getReferenceVolatile 按「对象指针 + 偏移」直接读具有 volatile 语义的对象引用,没有类型检查、没有反射开销,因此比 Field.get 快得多。堆外内存侧 allocateMemory/freeMemory 分配的内存不受 GC 管理——必须手动 freeMemory(),否则泄漏。这正好回答了「Unsafe 为什么叫 Unsafe」:它把 Java 最引以为傲的两道保险——类型检查和 GC——都拆掉了,直接按「对象指针+偏移」算地址读写,指针算错一个字节就是越界写、内存踩踏甚至 JVM 崩溃。

(注:本方法在旧 JDK 中叫 getObjectVolatile,引入 Valhalla 值类型后改名为 getReferenceVolatile;同理 putObjectVolatile 也改名为 putReferenceVolatile。)

VarHandle(JDK 9+):Unsafe 的类型安全替代

// JDK 9 VarHandle
MethodHandles.Lookup lookup = MethodHandles.lookup();
VarHandle handle = lookup.findVarHandle(MyClass.class, "value", int.class);

handle.compareAndSet(obj, expect, update);  // CAS
handle.getVolatile(obj);                     // volatile 读
handle.setRelease(obj, newVal);              // release 语义写

VarHandle 提供四种访问语义(Plain/Opaque/Release-Acquire/Volatile),比 Unsafe 的单一口径更精细,且类型安全(用泛型参数约束变量类型)。

总结

工具能力安全性
UnsafeCAS + park + 堆外内存 + 字段偏移❌ 绕过所有安全检查
VarHandleCAS + 内存排序语义✅ 类型安全
AtomicXxx包装好的原子操作✅ 最安全

章末提问

  1. Unsafe 为什么叫 Unsafe? 结论先行:它绕过类型检查、越界检查、GC 三道保险。因为直接按「对象指针+偏移」算地址读写,指针算错一个字节就是内存踩踏,堆外内存不受 GC 管理。

  2. CAS 底层是怎么保证原子性的? 结论先行:LOCK CMPXCHG 单条指令。因为 LOCK 前缀锁住总线/缓存行,CMPXCHG 在一条指令内完成「比较+交换」,别的核插不进手。

  3. park/unpark 相比 wait/notify 强在哪? 结论先行:不需要持 Monitor、unpark 可提前调用、只唤醒指定线程不惊群。因为 unpark 的许可可以累积到下次 park,唤醒目标精确到单个线程。

  4. 为什么 JDK 9 用 VarHandle 替代 Unsafe? 结论先行:类型安全 + 更精细的内存访问语义。因为 VarHandle 用泛型约束变量类型,并提供 Plain/Opaque/Release-Acquire/Volatile 四档访问语义。

  5. 堆外内存为什么必须手动释放? 结论先行:它不受 GC 管理。因为 allocateMemory 分配在 JVM 堆之外,GC 回收不到,忘记 freeMemory 就是永久泄漏。


Share this post on:

Previous Post
volatile的内存屏障——为什么DCL必须volatile
Next Post
TransmittableThreadLocal:跨线程池的上下文传递