怎么利用 Java 的 VarHandle 提供的 compareAndExchange 方法实现比传统 CAS 更灵活的内存语义
适用于无锁编程。VarHandle的compareAndExchange返回当前值(即旧值),而非仅成功标志,便于实现版本号乐观锁。它支持Acquire/Release等内存顺序以优化性能。需注意正确处理返回值;对final字段及32位JVM下long/double有限制。性能与Unsafe相当但依赖JIT内联,否则退化为JNI调用。
先看一个最核心的区别:compareAndExchange 总是返回当前值,不管交换成功与否;而 compareAndSet 只告诉你成功还是失败。这意味着,当你需要知道“为什么没换成功”——比如值被其他线程改了,或者压根没变——前者能直接给你答案。特别是实现带版本号的乐观锁时,不仅能知道更新失败,还能拿到当前版本号,用来决定下一步是重试、降级还是报错。

compareAndExchange 和 compareAndSet 语义差异在哪
需要注意,compareAndExchange 并不是 compareAndSet 的加强版,而是语义上更中立的原子读-改-写操作。一个常见的错误是把它当 compareAndSet 用,却忽略了返回值:
handle.compareAndExchange(obj, expected, desired); // ❌ 忽略返回值,等于白调
正确的做法,是显式检查返回值是否等于 expected,而不是依赖布尔逻辑。
如何用 VarHandle 实现 acquire-release 语义的 CAS
Ja va 的 VarHandle 允许为每次操作指定内存顺序,compareAndExchange 支持四种组合:Acquire、Release、AcquireRelease、SequentiallyConsistent。默认是 SequentiallyConsistent,性能开销最大;但如果你只关心“写后读可见”或“读后写不重排”,完全可以降级。
compareAndExchangeAcquire:保证后续读操作不会被重排到该操作之前compareAndExchangeRelease:保证前面的写操作不会被重排到该操作之后- 混合使用时(比如先用
Acquire读状态,再用Release更新),比全序模型减少 fence 指令,吞吐自然就上去了
需要警惕的是,这些方法不是重载,而是独立的方法名——不存在“传参数选语义”的方式。用错方法名会导致语义不符合预期,而且编译器不会提醒你。
compareAndExchange 在非基本类型字段上的限制
VarHandle 的 compareAndExchange 对引用类型字段(Object)完全支持,但对数组元素或嵌套字段(比如 obj.field.subfield)需要额外构造 handle。更关键的坑是:它不支持对 long/double 字段在 32 位 JVM 上的原子操作——虽然现代 JDK 基本都跑在 64 位上,但万一目标环境不明确,仍然可能触发 IllegalStateException。
典型的踩坑点包括:
- 用
static final VarHandle VH_LONG = MethodHandles.privateLookupIn(...).findVarHandle(...)构造 handle 时,没校验VH_LONG.isAccessModeSupported(VarHandle.AccessMode.COMPARE_AND_EXCHANGE) - 在 record 或 sealed class 的 final 字段上尝试
compareAndExchange——直接抛UnsupportedOperationException - 对 volatile 字段同时用
synchronized和compareAndExchange,造成语义冗余甚至死锁风险
和 Unsafe.compareAndSwapXxx 相比,实际性能差多少
VarHandle.compareAndExchange 在 HotSpot 上最终会编译为相同的汇编指令(在 x86 上就是 cmpxchg),所以单次操作延迟几乎一致。真正的差异出现在 JIT 优化层面:
- 反射式创建的
VarHandle(通过MethodHandles.lookup().findVarHandle(...))会有首次调用开销,后续被内联后没有差别 - 静态常量
VarHandle(static final)可被完全内联,性能与Unsafe持平 Unsafe.compareAndSwapInt等方法在 JDK 9+ 已标记为@Deprecated(forRemoval = true),部分 JVM(比如 GraalVM Native Image)根本不支持
说到底,影响性能的不是方法本身,而是你能不能帮 JIT 看清模式:比如把 compareAndExchange 写在循环里却不加 final 或稳定类型提示,结果可能导致去优化。
核心思路很简单:别只盯着“能不能换成功”,重点是你拿到的旧值怎么用;内存语义不是越强越好,而是刚好够用——AcquireRelease 覆盖大多数状态机跃迁场景,过度使用 SequentiallyConsistent 会悄悄拖慢吞吐。


































