★ JMM 是什么?volatile 如何保证可见性与有序性?
结论先行:Java 内存模型(JMM)规定共享变量存在于主内存,每条线程在自己的工作内存中操作副本,并靠 happens-before 规则约束重排序。volatile 的语义有两条:写操作对其他线程的读立即可见;禁止相关指令重排序(通过插入内存屏障实现)。但它不保证原子性。
三大特性与 volatile 的关系
| 并发特性 | 含义 | volatile 能否保证 |
|---|---|---|
| 原子性 | 操作不可再分 | 否(如 i++ 仍非原子) |
| 可见性 | 一线程修改,其他线程立即可见 | 能(写刷主存、读失效本地副本) |
| 有序性 | 指令按预期顺序执行 | 能(禁止重排序 + 内存屏障) |
记忆:volatile 只防“走样”,不防“拆分”——i++ 这类读-改-写操作仍需要锁或原子类兜底。
保证机制拆解
- 可见性:volatile 写会强制把新值刷新回主内存;volatile 读会强制从主内存读取,使其他线程的写入立即可见。
- 内存屏障:volatile 写前插入 StoreStore、写后插入 StoreLoad;读操作前后配合 LoadLoad/LoadStore,划出重排序禁区。
- happens-before 规则:对 volatile 变量的写,happens-before 后续对该变量的每一次读。
- 典型应用:状态标志位、单例双重检查锁、发布不可变对象;计数类读改写请交给 Atomic 或锁。
public class VolatileDemo {
private static volatile boolean stop = false; // 保证修改对其他线程可见
public static void main(String[] args) throws Exception {
Thread worker = new Thread(() -> {
while (!stop) { /* 忙等;若无 volatile 可能永远读到旧值 */ }
System.out.println("stopped");
});
worker.start();
Thread.sleep(100);
stop = true; // 不声明 volatile,主线程的修改可能不被 worker 感知
}
}
常见追问 / 记忆点
- 记忆点:volatile = 可见性 + 有序性,缺原子性;经典场景是状态标志与单例双重检查锁。
- 追问:双重检查锁单例的 instance 必须 volatile,否则可能拿到“半初始化”的对象。
- 追问:synchronized 三者都保证;Lock 通过其内存语义同样能保证可见性,只是写法更繁琐。
- 追问:final 字段有额外内存语义——构造完成后对其他线程可见,常与 volatile 互补使用。