Java 内存模型(JMM)与 volatile、synchronized 可见性原理
文章目录
- 《Java 内存模型(JMM)与 volatile、synchronized 可见性原理》
- 一、前言:为什么需要 Java 内存模型(JMM)
- 二、JMM 内存结构与线程交互过程
- (1)整体结构示意
- (2)JMM 规定的 8 种原子操作
- 三、happens-before 原则(JMM 的核心规则)
- 四、volatile 关键字详解
- (1)volatile 的作用
- (2)底层实现(内存屏障)
- (3)volatile 的局限
- 五、synchronized 与内存可见性
- (1)锁的语义
- (2)内存可见性
- (3)与 volatile 的区别
- 六、面试高频问题与答题模板
- 结语
《Java 内存模型(JMM)与 volatile、synchronized 可见性原理》
一、前言:为什么需要 Java 内存模型(JMM)
在多线程环境下,每个线程都有自己的工作内存(CPU缓存),
而共享变量存放在主内存中。
问题是:
一个线程修改变量后,其他线程可能看不到最新值。
原因:
- CPU 缓存导致“可见性问题”;
- 编译器和 CPU 指令优化导致“重排序问题”;
- 缺乏同步导致“原子性问题”。
于是,JVM 定义了 Java 内存模型(Java Memory Model, JMM),
用来规定多线程访问共享变量时的行为规范,
即:什么时候写入主内存、什么时候刷新到工作内存、哪些操作是有序的。
二、JMM 内存结构与线程交互过程
(1)整体结构示意
┌──────────────────────────────┐│ 主内存(Main Memory)││ 共享变量、对象实例、静态区 │└───────────┬──────────────────┘│┌────────────────┴────────────────┐│ │
┌──────────────────────┐ ┌──────────────────────┐
│ 线程 A 的工作内存 │ │ 线程 B 的工作内存 │
│ 寄存器 + CPU 缓存 │ │ 寄存器 + CPU 缓存 │
└──────────────────────┘ └──────────────────────┘
每个线程:
- 从主内存读取变量副本到工作内存;
- 修改后再写回主内存;
- 线程间 不直接共享工作内存。
(2)JMM 规定的 8 种原子操作
JMM 定义了主内存与工作内存的交互规则:
| 操作 | 含义 |
|---|---|
| lock | 锁定变量,标识为线程独占 |
| unlock | 解锁变量,使其他线程可见 |
| read | 从主内存读取变量值 |
| load | 将值载入工作内存 |
| use | 执行代码时使用变量 |
| assign | 给变量赋值 |
| store | 将变量值写入主内存副本 |
| write | 将 store 的值写回主内存 |
这些操作保证多线程访问的可见性与顺序性。
三、happens-before 原则(JMM 的核心规则)
JMM 并不强制“物理执行顺序”,
而是通过 happens-before 原则 定义“逻辑可见性关系”。
| happens-before 规则 | 含义 |
|---|---|
| 程序顺序规则 | 单线程内,代码按顺序执行 |
| 监视器锁规则 | 对同一锁的 unlock 操作先行于后续的 lock |
| volatile 变量规则 | 对 volatile 的写先行于后续对它的读 |
| 线程启动规则 | Thread.start() 先行于线程体内的操作 |
| 线程终止规则 | 线程结束(join)先行于 join 返回 |
| 传递性规则 | A → B, B → C ⇒ A → C |
举例:
int a = 0;
boolean flag = false;Thread t1 = new Thread(() -> {a = 1;flag = true;
});Thread t2 = new Thread(() -> {if (flag) System.out.println(a);
});
若 flag 为 volatile,则:
t1对a的写入 happens-beforet2对flag的读取。
保证 t2 看到 a=1,而不是旧值。
四、volatile 关键字详解
(1)volatile 的作用
- 保证可见性:修改立即刷新到主内存;
- 禁止指令重排:在读写操作前后插入内存屏障;
- 不保证原子性:非复合操作仍可能被打断。
(2)底层实现(内存屏障)
JIT 编译后,volatile 读写会插入内存屏障指令(Memory Barrier):
| 屏障类型 | 位置 | 作用 |
|---|---|---|
| StoreStoreBarrier | 写操作前 | 保证写入主内存前先完成前序写入 |
| StoreLoadBarrier | 写操作后 | 禁止写后读乱序 |
| LoadLoadBarrier | 读操作前 | 保证读取最新值 |
| LoadStoreBarrier | 读操作后 | 防止读后写乱序 |
JVM 通过这些屏障确保“先写再读”的可见性与顺序性。
(3)volatile 的局限
volatile int count = 0;
count++; // 非原子操作
count++ 实际包含三步:
- 读取 count;
- 自增;
- 写回内存。
→ 线程切换可能导致值覆盖,仍需使用AtomicInteger或synchronized。
五、synchronized 与内存可见性
(1)锁的语义
synchronized 同步块在 JVM 层面基于 Monitor 实现。
进入同步块相当于:
lock + read/load + use + assign + store/write + unlock
(2)内存可见性
进入 synchronized:
- 线程会从主内存刷新共享变量;
- 执行结束后将修改写回主内存;
- 确保锁内代码对后续线程可见。
(3)与 volatile 的区别
| 特性 | volatile | synchronized |
|---|---|---|
| 可见性 | ✅ 保证 | ✅ 保证 |
| 原子性 | ❌ 不保证 | ✅ 保证 |
| 有序性 | ✅ 保证 | ✅ 保证 |
| 实现方式 | 内存屏障 | Monitor 锁 |
| 性能 | 轻量 | 重量(可优化为偏向/轻量级锁) |
六、面试高频问题与答题模板
| 问题 | 答案要点 |
|---|---|
| Q1:JMM 是什么? | Java Memory Model,定义多线程读写共享变量的可见性和顺序性规范。 |
| Q2:JMM 为什么存在? | 为屏蔽 CPU 缓存与指令重排带来的并发不一致问题。 |
| Q3:volatile 解决了什么问题? | 保证可见性与有序性,不保证原子性。 |
| Q4:synchronized 如何保证可见性? | 通过进入/退出 monitor 实现内存同步(刷新主内存)。 |
| Q5:volatile 底层实现? | 通过内存屏障指令禁止重排序(StoreLoadBarrier)。 |
| Q6:happens-before 是什么? | 定义逻辑上的可见性顺序,不等同于物理执行顺序。 |
| Q7:volatile 适用于哪些场景? | 状态标志、单例双重校验锁、轻量读写控制。 |
结语
JMM 是理解所有并发机制的理论根基。
从 volatile 到 synchronized,再到 AQS、CAS、LockSupport,
它们的本质都是围绕三件事展开:
可见性、有序性、原子性。
掌握 JMM,你就能从“语法层并发”跃升到“CPU 层理解”,
真正能解释为什么一行 volatile 能让系统稳定运行。
下一篇,我将写——
《Redis 高并发八股文:缓存穿透、击穿、雪崩与解决方案》,
进入高并发实战篇,讲清 Redis 三大核心问题的区别、原理与企业级防护方案。
