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 的单一口径更精细,且类型安全(用泛型参数约束变量类型)。
总结
| 工具 | 能力 | 安全性 |
|---|---|---|
| Unsafe | CAS + park + 堆外内存 + 字段偏移 | ❌ 绕过所有安全检查 |
| VarHandle | CAS + 内存排序语义 | ✅ 类型安全 |
| AtomicXxx | 包装好的原子操作 | ✅ 最安全 |
章末提问
-
Unsafe 为什么叫 Unsafe? 结论先行:它绕过类型检查、越界检查、GC 三道保险。因为直接按「对象指针+偏移」算地址读写,指针算错一个字节就是内存踩踏,堆外内存不受 GC 管理。
-
CAS 底层是怎么保证原子性的? 结论先行:LOCK CMPXCHG 单条指令。因为 LOCK 前缀锁住总线/缓存行,CMPXCHG 在一条指令内完成「比较+交换」,别的核插不进手。
-
park/unpark 相比 wait/notify 强在哪? 结论先行:不需要持 Monitor、unpark 可提前调用、只唤醒指定线程不惊群。因为 unpark 的许可可以累积到下次 park,唤醒目标精确到单个线程。
-
为什么 JDK 9 用 VarHandle 替代 Unsafe? 结论先行:类型安全 + 更精细的内存访问语义。因为 VarHandle 用泛型约束变量类型,并提供 Plain/Opaque/Release-Acquire/Volatile 四档访问语义。
-
堆外内存为什么必须手动释放? 结论先行:它不受 GC 管理。因为 allocateMemory 分配在 JVM 堆之外,GC 回收不到,忘记 freeMemory 就是永久泄漏。