Java 内存模型与可见性实践
JMM(Java Memory Model)定义了线程间可见性、原子性与有序性。本文通过可复现案例解释 volatile、happens-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_methods、jfr观测热点与锁竞争。
6. JMM 与内存屏障的演进(JSR-133)
- JDK 5 以前(JSR-133 之前),内存模型对
volatile与final的约束较弱,双检锁(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 xchg、lock add0),或显式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读:LDARvolatile写:STLRVarHandle.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 可用
mfence或lock原子操作;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 验证关键点的指令是否达成预期语义(仅在关键问题排查时使用)。