一、JMM 基础
Q1:什么是 JMM?
答: JMM(Java Memory Model,Java 内存模型)是 Java 虚拟机规范中定义的一套多线程内存访问规则,用于屏蔽不同硬件和操作系统的内存差异。
核心目标:定义多线程程序中,共享变量的可见性、有序性和原子性规则。
JMM 是抽象概念,不是真实存在的内存,而是一套规范:
线程 A 线程 B
│ │
工作内存A 工作内存B
│ │
└──── 主内存 ────────┘主内存:所有线程共享,存储共享变量
工作内存:每个线程私有,存储共享变量副本
Q2:JMM 中的主内存和工作内存是什么?
答:
交互规则:
线程操作共享变量,必须先拷贝到工作内存
操作完成后,写回主内存
线程间无法直接访问对方工作内存
// 线程 A 修改变量,线程 B 可能看不到
static boolean flag = false;
// 线程 A
flag = true; // 写工作内存,未刷主内存
// 线程 B
while (!flag) {} // 读的是工作内存的旧值Q3:JMM 的三大特性是什么?
答:
// 原子性:count++ 非原子
count++;
// 可见性:volatile 保证
volatile boolean flag;
// 有序性:synchronized 保证
synchronized (lock) { ... }Q4:JMM 和 JVM 内存结构有什么区别?
答: 这是常考易混点:
JMM 是"规则",JVM 内存结构是"物理布局"。
JMM:主内存 ←→ 工作内存(抽象)
JVM:堆、栈、方法区、程序计数器(具体)Q5:JMM 中的八种原子操作是什么?
答: JMM 定义了 8 种操作,用于主内存和工作内存交互:
流程:
read → load → use (读)
assign → store → write (写)规则:read/load、store/write 必须成对出现。
二、happens-before 原则
Q6:什么是 happens-before 原则?
答: happens-before 是 JMM 定义的偏序关系,用于判断数据竞争和可见性:
如果 A happens-before B,则 A 的结果对 B 可见
不要求 A 在时间上先于 B执行,只要求 A 的结果对 B 可见
int x = 1; // A
int y = x + 1; // B
// A hb B,B 一定能看到 x=1核心作用:让开发者不用关心底层重排,只要满足 hb 规则,就保证正确性。
Q7:happens-before 有哪些规则?
答: 主要有 8 条:
// 程序顺序
int a = 1; // A
int b = 2; // B,A hb B
// volatile
volatile int x;
x = 1; // 写
int y = x; // 读,写 hb 读
// 传递性
A hb B, B hb C → A hb CQ8:程序顺序规则一定保证代码按顺序执行吗?
答: 不一定。程序顺序规则只保证 happens-before 关系,不保证物理执行顺序:
int a = 1;
int b = 2;
int c = a + b;
// a=1 和 b=2 可能重排
// 但 c = a + b 一定在两者之后关键:
hb 规则保证结果正确
单线程内重排只要不影响结果就允许(as-if-serial)
多线程间需要 hb 规则约束
Q9:volatile 的 happens-before 规则是什么?
答: volatile 写 hb 后续的 volatile 读:
volatile boolean flag = false;
int data = 0;
// 线程 A
data = 1; // 1
flag = true; // 2,volatile 写
// 线程 B
if (flag) { // 3,volatile 读
int x = data; // 4,一定能看到 data=1
}推导:
data = 1hbflag = true(程序顺序)flag = truehbif(flag)(volatile 规则)传递性:
data = 1hbint x = data
结论:volatile 写之前的操作,对 volatile 读之后的操作可见。
Q10:监视器锁的 happens-before 规则是什么?
答: 解锁 hb 后续加锁:
synchronized (lock) { // 加锁
data = 1;
} // 解锁
// 线程 B
synchronized (lock) { // 加锁,能看到 data=1
int x = data;
}含义:
线程 A 解锁前的所有操作
对线程 B 加锁后的所有操作可见
本质:锁的释放-获取建立了 hb 关系。
Q11:线程启动和终止的 hb 规则是什么?
答:
启动规则:start() hb 子线程所有操作
int x = 1;
new Thread(() -> {
System.out.println(x); // 一定看到 x=1
}).start();终止规则:线程所有操作 hb join() 返回
Thread t = new Thread(() -> {
x = 1;
});
t.start();
t.join(); // join 返回后
System.out.println(x); // 一定看到 x=1用途:安全的线程间数据传递。
Q12:happens-before 和 as-if-serial 有什么区别?
答:
关系:
as-if-serial 是单线程的重排原则
happens-before 是多线程的可见性原则
两者共同保证程序正确性
Q13:happens-before 能保证原子性吗?
答: 不能。happens-before 只保证可见性和有序性:
volatile int count = 0;
// 10 个线程各 ++ 1000 次
count++; // 非原子,结果 < 10000原因:
hb 保证写对后续读可见
但
count++是读-改-写三步多线程可能交叉执行
解决:synchronized、Lock、原子类。
Q14:如何用 happens-before 分析 DCL 单例?
答:
public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}分析:
线程 A 创建对象,
instance = new Singleton()volatile 写 hb 后续 volatile 读
线程 B 读
instance,一定看到完整初始化
不加 volatile 的问题:
new分三步:分配内存、初始化、赋值引用2 和 3 可能重排
线程 B 可能拿到半初始化对象
三、内存屏障
Q15:什么是内存屏障?
答: 内存屏障(Memory Barrier)是 CPU 指令,用于:
禁止特定指令重排
强制刷新缓存,保证可见性
本质:在指令之间插入"栅栏",禁止跨越。
// 伪代码
store(x);
StoreLoadBarrier(); // 屏障
load(y);Q16:内存屏障有哪几种?
答: 四种基本类型:
记忆:
前一个操作 + 后一个操作
LoadLoad、StoreStore、LoadStore、StoreLoad
Q17:volatile 用了哪些内存屏障?
答:
volatile 写:
StoreStore 屏障
volatile 写
StoreLoad 屏障volatile 读:
LoadLoad 屏障
volatile 读
LoadStore 屏障完整示例:
// volatile 写
int a = 1;
// StoreStore
volatile int v = 2;
// StoreLoad
// volatile 读
// LoadLoad
int x = v;
// LoadStore
int b = 3;作用:
StoreStore:保证前面写先刷主内存
StoreLoad:保证 volatile 写后其他线程能读到
Q18:为什么 StoreLoad 屏障最重?
答: 因为 StoreLoad 需要刷新写缓冲区:
其他三种屏障在 x86 上通常是空操作
StoreLoad 需要
mfence指令强制等待写缓冲区刷入主内存
可能触发缓存一致性协议(MESI)
// x86 上的实现
// LoadLoad、StoreStore、LoadStore:空操作
// StoreLoad:mfence 指令结论:volatile 写比读开销大。
Q19:内存屏障和 volatile 的关系是什么?
答: volatile 通过内存屏障实现三大语义:
// volatile 写的底层
mov [v], 1
lock addl $0, (%rsp) // StoreLoad 屏障lock 前缀指令:
刷新缓存
使其他 CPU 缓存失效
禁止重排
Q20:final 字段有内存屏障吗?
答: 有。JMM 对 final 有特殊保证:
写 final 字段:
StoreStore 屏障(在 final 写之后)读 final 字段:
LoadLoad 屏障(在 final 读之前)作用:
保证 final 字段在构造完成后,对其他线程可见
禁止 final 写重排到构造外
public class Config {
final int x;
public Config() {
x = 1; // StoreStore 屏障保证
}
}Q21:synchronized 用了哪些内存屏障?
答:
monitorenter:插入 LoadLoad + LoadStore 屏障
monitorexit:插入 StoreStore + StoreLoad 屏障
synchronized (lock) {
// monitorenter 后:LoadLoad + LoadStore
// 代码
// monitorexit 前:StoreStore + StoreLoad
}效果:
加锁:清空工作内存,从主内存重新加载
解锁:把工作内存刷新到主内存
本质:synchronized 通过 Monitor 的进入/退出实现内存屏障效果。
Q22:内存屏障在硬件层面如何实现?
答:
x86 特点:
强内存模型,大部分屏障是空操作
只需 StoreLoad 显式指令(mfence)
用
lock前缀指令也可实现
// x86 上 volatile 写编译后
mov [v], 1
lock addl $0, (%rsp) // 相当于 mfenceQ23:内存屏障和缓存一致性协议的关系?
答:
缓存一致性协议(MESI):保证多核缓存数据一致
内存屏障:保证指令顺序和可见性
配合:
写 volatile:lock 指令让其他缓存失效
其他 CPU 发现缓存失效,重新从主内存加载
内存屏障保证指令不重排
CPU A 写 → lock → 缓存失效 → CPU B 重新加载MESI 状态:
Modified:已修改
Exclusive:独占
Shared:共享
Invalid:失效
Q24:内存屏障会影响性能吗?
答: 会,但影响有限:
优化:
x86 上只有 StoreLoad 有开销
读多写少场景,volatile 影响小
高并发写场景,考虑减少 volatile 写
结论:volatile 是轻量级同步,性能远好于锁。
四、综合与实战
Q25:JMM 如何保证可见性?
答:
volatile int x = 1; // 写刷主内存
int y = x; // 读从主内存加载Q26:JMM 如何保证有序性?
答:
// volatile 禁止重排
int a = 1; // 1
volatile int b = 2; // 2
// 1 不会重排到 2 之后
// synchronized 保证临界区有序
synchronized (lock) {
// 代码按顺序执行
}Q27:JMM 如何保证原子性?
答:
// 非原子
count++;
// 原子
synchronized (lock) { count++; }
atomicInteger.incrementAndGet();注意:JMM 不保证普通变量的复合操作原子性。
Q28:为什么说 JMM 是"抽象"的?
答: 因为 JMM 不规定具体实现,只定义规则:
主内存/工作内存是抽象概念
不同硬件有不同实现
JMM 屏蔽差异,保证跨平台一致
// 同一段代码,不同 CPU 架构
// JMM 保证行为一致
volatile int x = 1;类比:JMM 像"接口",硬件实现像"实现类"。
Q29:JMM 和硬件内存模型有什么关系?
答:
关系:
JMM 建立在硬件模型之上
通过内存屏障适配不同硬件
强模型(x86)屏障开销小
弱模型(ARM)屏障开销大
Q30:如何用 JMM 解释 volatile 可见性?
答: 完整推导:
线程 A 写 volatile:
把工作内存的值刷到主内存
插入 StoreLoad 屏障
线程 B 读 volatile:
清空工作内存中该变量的副本
从主内存重新加载
插入 LoadLoad 屏障
happens-before:
volatile 写 hb 后续 volatile 读
// 线程 A
data = 1; // 普通写
flag = true; // volatile 写,刷新主内存
// 线程 B
if (flag) { // volatile 读,从主内存加载
assert data == 1; // 成立
}Q31:为什么双重检查锁需要 volatile?
答: 防止指令重排导致半初始化对象:
public class Singleton {
private static volatile Singleton instance;
// ...
}不加 volatile 的问题:
instance = new Singleton();
// 实际分三步:
// 1. 分配内存
// 2. 初始化对象
// 3. instance 指向内存
// 重排后:1 → 3 → 2
// 线程 B 在 3 之后、2 之前读到 instance != null
// 但对象还未初始化volatile 作用:
禁止 2 和 3 重排
保证安全发布
Q32:JMM 中的"数据竞争"是什么?
答: 数据竞争指多个线程同时访问同一变量,且至少有一个是写操作,且没有同步:
// 数据竞争
int x = 0;
// 线程 A 线程 B
x = 1; int y = x;
// 无同步,y 可能为 0 或 1判断依据:
两个操作访问同一变量
至少一个是写
没有 happens-before 关系
后果:结果不确定,可能出错。
Q33:如何避免数据竞争?
答:
使用 volatile(简单标志)
使用 synchronized(临界区)
使用 Lock(高级控制)
使用原子类(简单计数)
不可变对象(final)
// 方案1:volatile
volatile boolean flag;
// 方案2:synchronized
synchronized (lock) { x++; }
// 方案3:原子类
AtomicInteger count = new AtomicInteger();Q34:JMM 中"安全发布"是什么意思?
答: 安全发布指对象构造完成后,才被其他线程看到:
不安全发布:
public class Holder {
int x;
public Holder(int x) { this.x = x; }
}
// 线程 A
holder = new Holder(1); // 可能重排
// 线程 B
if (holder != null) {
holder.x; // 可能看到 0
}安全发布方式:
final 字段
volatile 引用
synchronized
静态初始化器
// 安全发布
private static volatile Holder holder;Q35:JMM 和 JSR-133 有什么关系?
答: JSR-133 是 JMM 的修订规范(Java 5 引入):
修复了旧 JMM 的缺陷
引入 happens-before 原则
明确 volatile 语义
增强 final 语义
核心改进:
volatile 写 hb 读
final 字段安全发布
禁止 synchronized 内的重排
意义:让多线程编程更可靠。
Q36:JMM 中的"字撕裂"是什么?
答: 字撕裂(Word Tearing)指 64 位变量在 32 位 JVM 上被拆分读写:
// 32 位 JVM 上
long x = 0xFFFFFFFFL;
// 需要两次 32 位写
// 其他线程可能读到"半个"值JMM 规定:
普通 long/double 读写不保证原子
volatile long/double 读写保证原子
引用类型读写保证原子
解决:用 volatile 或 AtomicLong。
Q37:JMM 如何保证 final 字段的可见性?
答: 通过两条规则:
写 final:禁止重排到构造外
构造内所有写 → StoreStore → final 写 → 构造结束读 final:禁止重排到读之前
读对象引用 → LoadLoad → 读 final 字段public class Config {
final int x;
final int y;
public Config() {
x = 1; // 不会重排到构造外
y = 2;
}
}
// 其他线程看到 config 引用时,x、y 一定已初始化Q38:JMM 中的"顺序一致性"是什么?
答: 顺序一致性是理想的内存模型:
所有操作按程序顺序执行
所有线程看到相同的执行顺序
每个操作原子执行
JMM 不保证顺序一致性:
允许重排
通过 happens-before 约束
比顺序一致性更弱,但性能更好
// 顺序一致性:严格按顺序
// JMM:允许重排,但保证结果正确Q39:JMM 和 CAS 有什么关系?
答: CAS 是 JMM 原子性的实现基础:
CAS 底层用 CPU 的
cmpxchg指令配合 volatile 保证可见性
是原子类、AQS 的基础
public class AtomicInteger {
private volatile int value; // 可见性
// CAS 保证原子性
public final boolean compareAndSet(int e, int u) {
return U.compareAndSwapInt(this, VALUE, e, u);
}
}组合:volatile(可见性)+ CAS(原子性)= 无锁并发。
Q40:JMM 的未来发展趋势?
答:
VarHandle(JDK 9):更灵活的内存访问
Foreign Memory API:堆外内存
Loom 项目:虚拟线程
值类型:更高效的内存布局
// VarHandle 示例
VarHandle vh = MethodHandles.lookup()
.findVarHandle(MyClass.class, "x", int.class);
vh.setVolatile(obj, 1);核心:JMM 持续演进,但 happens-before 原则不变。
评论