Java 内存模型与可见性实践

JMM(Java Memory Model)定义了线程间可见性、原子性与有序性。本文通过可复现案例解释 volatilehappens-before、指令重排与发布/逃逸,并给出 JIT 层面的观测方法。

1. 可见性问题复现

public class StopFlagDemo {
  // 去掉 volatile 观察现象
  static /*volatile*/ boolean stop = false;
  public static void main(String[] args) throws Exception {
    new Thread(() -> { while (!stop) { } }).start();
    Thread.sleep(100);
    stop = true;
  }
}

volatile 时,JIT 可能将 stop 缓存至寄存器,循环无法观察到主线程写入。

2. JIT 观测(hsdis/PrintAssembly)

启动参数:

-XX:+UnlockDiagnosticVMOptions -XX:+PrintCompilation -XX:+PrintAssembly

观察 volatile 写前后的屏障指令(如 x86 的 lock 前缀原子指令 / mfence)。

3. 指令重排与双检锁

class LazySingleton {
  private static volatile LazySingleton INSTANCE;
  static LazySingleton get() {
    if (INSTANCE == null) {
      synchronized (LazySingleton.class) {
        if (INSTANCE == null) INSTANCE = new LazySingleton();
      }
    }
    return INSTANCE;
  }
}

volatile 避免构造重排导致的“引用可见但对象未完成初始化”。

4. Happens-Before 速查

  • 程序次序、监视器锁、volatile、线程启动/终止、传递性。

5. 排错清单

  • 自旋等待变量需 volatile 或原子类;
  • 不要在锁外读写与锁内读写混用同一状态;
  • 使用 jcmd VM.print_touched_methodsjfr 观测热点与锁竞争。

6. JMM 与内存屏障的演进(JSR-133)

  • JDK 5 以前(JSR-133 之前),内存模型对 volatilefinal 的约束较弱,双检锁(DCL)可能失效。
  • 自 JDK 5(JSR-133)起:
    • volatile 写建立 Store-Store 与 Store-Load 语义;volatile 读建立 Load-Load 与 Load-Store 语义(等价于写的释放、读的获取)。
    • final 字段发布语义明确:构造完成后对其它线程可见,避免“半初始化”。
  • 抽象屏障分类(便于理解,具体实现依赖平台):
    • LoadLoad, LoadStore, StoreStore, StoreLoad
    • 一般而言:volatile 写 ~ Release(StoreStore + StoreLoad),volatile 读 ~ Acquire(LoadLoad + LoadStore)。

7. HotSpot 在不同硬件上的实现(概览)

不同平台的内存一致性与可用指令不同,HotSpot 会在 C1/C2 后端针对性插入屏障/选用原子指令。

7.1 x86/x64(TSO)

  • 天然较强(Total Store Order):已保证 LoadLoad / LoadStore,有风险的是 StoreLoad。
  • 常见映射(HotSpot 版本与编译后端可能差异):
    • volatile 写:使用带 lock 前缀的原子指令(如 lock xchglock add 0),或显式 mfence,以形成 Full Fence,确保写-读不可越过。
    • volatile 读:通常普通 mov 即可满足 Acquire 语义(TSO 已保证读的顺序),必要时可能配合轻量栅栏。
  • CAS/原子操作:lock cmpxchg,天然携带全栅栏语义。

示例(节选,不同 JDK/编译器可能略有不同,仅供识别思路):

; volatile store 之前/之后的栅栏
lock addl $0x0, (%rsp)   ; 作为全栅栏(或使用 mfence)
mov DWORD PTR [field], eax ; 实际写入

7.2 AArch64(ARMv8)

  • 指令级提供获取/释放语义:
    • LDAR(acquire load)/ STLR(release store)
    • 显式全栅栏:DMB ish
  • 常见映射:
    • volatile 读:LDAR
    • volatile 写:STLR
    • VarHandle.fullFence() / Unsafe.fullFence()DMB ish

7.3 其它(简述)

  • ARMv7:ldrex/strex + dmb 组合;
  • PPC:lwsync(轻量栅栏)、sync(全栅栏)。

关键点:JMM 语义是上层抽象,HotSpot 在不同平台选用“能满足该语义的最弱指令组合”,以减少开销。

8. VarHandle 与 Unsafe 的屏障对应

现代 JDK 推荐使用 VarHandle 指定精确的内存语义:

  • 访问语义:getAcquire / setRelease / getOpaque / setOpaque / getVolatile / setVolatile
  • 栅栏语义:VarHandle.acquireFence()releaseFence()fullFence()

大致映射关系:

  • Acquire/Release -> 对应硬件 acquire/release(如 AArch64 的 LDAR/STLR);
  • Volatile -> acquire + release(按点位插入栅栏,或选用更强指令);
  • FullFence -> 全序列栅栏(x86 可用 mfencelock 原子操作;AArch64 用 dmb ish)。

Unsafe(不推荐新代码使用)也提供:storeFence()loadFence()fullFence(),与 VarHandle 栅栏语义对应。

9. hsdis 观测指引(示例)

volatile 写为例,打开 -XX:+PrintAssembly 后,在输出中搜索目标方法:

; ...
; 关键位置常见:
lock addl $0x0,(%rsp)   ; 或 mfence,形成 StoreLoad 屏障
mov    %eax,0xNN(%rbx)  ; 写入 volatile 字段
; ...

在 AArch64:

stlr    w0, [x1]        ; release store 到 volatile 字段

说明:不同 JDK/编译器(C1/C2)、优化级别与 CPU 平台会有差异,识别“是否具备 acquire/release/全栅栏”语义是核心。

10. 实战建议(落地)

  • 选择合适语义:
    • 只需要“发布事件/状态位”的写:setRelease;对应读取用 getAcquire
    • 必须与既有 volatile 交互且要求最强语义:使用 getVolatile/setVolatile
    • 仅跨线程传递“可能乱序但最终一致”的值:getOpaque/setOpaque(更弱,开销小)。
  • 避免无谓全栅栏:全栅栏成本高,在热点路径优先 acquire/release 组合。
  • 发布对象:
    • 使字段 final,或在构造完毕后通过 volatile/release 语义发布;
    • 避免“逃逸未完成初始化”的引用外泄。
  • 诊断与基线:
    • 用 JFR 观测锁竞争/线程状态;
    • 用 hsdis 验证关键点的指令是否达成预期语义(仅在关键问题排查时使用)。