VarHandle的getAcquire()和setRelease()是比volatile更底层、更可控的内存屏障替代方案,用于在无锁编程中精准建立happens-before关系,避免full volatile语义带来的性能开销。

VarHandle 的 getAcquire() 和 setRelease() 不是 volatile 的“升级版”,而是更底层、更可控的替代方案——它们让你在不引入 full volatile 语义的前提下,精准插入内存屏障,避免不必要的性能开销和重排序约束。
getAcquire() 和 setRelease() 解决什么问题
-
volatile字段读写默认带 full fence(读写都带 acquire + release),但很多场景只需单向同步:比如生产者写完数据再发布标志,消费者看到标志后才读数据——这时用setRelease()写标志 +getAcquire()读标志,就能保证之前所有写操作对后续读可见,且不阻止编译器/JIT 对标志读写前后的无关指令重排。 - 错误地混用语义会导致可见性断裂:比如用
setOpaque()写入后,另一个线程用普通get()读,就没有 happens-before 关系,data 可能仍是旧值。 - 典型适用场景:无锁队列的
tail更新、状态机跃迁、生产者-消费者信号量。
如何正确构造并使用带 Acquire/Release 语义的 VarHandle
-
必须通过
MethodHandles.lookup().findVarHandle()获取,不能直接反射字段或硬编码:private static final VarHandle VH_STATE; static { try { VH_STATE = MethodHandles.lookup() .findVarHandle(Counter.class, "state", int.class); } catch (Exception e) { throw new Error(e); } } 使用时注意类型匹配:字段必须是支持原子访问的类型(
int、long、引用类型等),且不能是double或未对齐的long(JVM 层面不保证原子性)。-
常见误用点:
- 把
VH_STATE.setRelease(obj, 1)当作“先写再刷缓存”,却忘了它不保证之前所有写都已提交到主内存——它只保证这些写不会被重排到该调用之后 - 在 ARM 等弱内存模型 CPU 上,漏掉
getAcquire()而用普通get(),会导致读不到最新状态
- 把
setRelease() + getAcquire() 的实际协作模式
这是最常用的配对,用于建立跨线程的单向同步链:
-
生产者侧:
- 先写业务数据(如
buffer[i] = value) - 再调用
VH_FLAG.setRelease(obj, 1)
- 先写业务数据(如
-
消费者侧:
- 循环调用
(int) VH_FLAG.getAcquire(obj)直到非零 - 此时可安全读取
buffer[i],JMM 保证其值已对当前线程可见
- 循环调用
-
关键点:
-
setRelease()不是“写完立刻全局可见”,而是“我写完的前面所有东西,对接下来用getAcquire()读到这个字段的线程可见” - 它比
volatile更轻量:不触发读屏障(reader 不需要 flush cache),也不强制写屏障(writer 不需要 sync all stores)
-
和 Interlocked、volatile 的关键差异在哪
-
volatile是语言级关键字,语义固定、不可拆分,且 C# 中仅支持有限类型(long、double不支持),而VarHandle是运行时构建的通用机制,支持任意字段(包括数组元素、嵌套字段)。 -
Interlocked.CompareExchange()提供原子读-改-写,但默认是 full fence;若你只需要“发布一个状态”,它会多付出不必要的同步成本。 -
VarHandle的getAcquire()/setRelease()是真正按需施加屏障,适合高频、低延迟场景(如 RingBuffer、MPSC 队列)。
复杂点在于:它要求你自己建模 happens-before 链。一个没配对的 getAcquire() 或错放的 setRelease(),不会报错,但会在 ARM 或 JVM 逃逸分析开启时暴露竞态——这种 bug 很难复现,也很难调试。

















