少女祈祷中...
NEKO
知识库

java 内存模型面试要点

一、JMM 基础

Q1:什么是 JMM?

答: JMM(Java Memory Model,Java 内存模型)是 Java 虚拟机规范中定义的一套多线程内存访问规则,用于屏蔽不同硬件和操作系统的内存差异。

核心目标:定义多线程程序中,共享变量的可见性、有序性和原子性规则。

JMM 是抽象概念,不是真实存在的内存,而是一套规范:

线程 A                线程 B
  │                    │
工作内存A           工作内存B
  │                    │
  └──── 主内存 ────────┘
  • 主内存:所有线程共享,存储共享变量

  • 工作内存:每个线程私有,存储共享变量副本


Q2:JMM 中的主内存和工作内存是什么?

答:

名称

说明

对应硬件

主内存

所有线程共享

物理内存

工作内存

线程私有

CPU 缓存/寄存器

交互规则:

  1. 线程操作共享变量,必须先拷贝到工作内存

  2. 操作完成后,写回主内存

  3. 线程间无法直接访问对方工作内存

// 线程 A 修改变量,线程 B 可能看不到
static boolean flag = false;

// 线程 A
flag = true;  // 写工作内存,未刷主内存

// 线程 B
while (!flag) {} // 读的是工作内存的旧值

Q3:JMM 的三大特性是什么?

答:

特性

含义

保障方式

原子性

操作不可分割

synchronized、Lock、原子类

可见性

修改立即被看到

volatile、synchronized、final

有序性

按代码顺序执行

volatile、synchronized、happens-before

// 原子性:count++ 非原子
count++;

// 可见性:volatile 保证
volatile boolean flag;

// 有序性:synchronized 保证
synchronized (lock) { ... }

Q4:JMM 和 JVM 内存结构有什么区别?

答: 这是常考易混点:

对比项

JMM

JVM 内存结构

层次

抽象规范

实际运行时区域

目的

多线程内存规则

内存分配与回收

内容

主内存/工作内存

堆、栈、方法区等

关注

可见性、有序性、原子性

对象分配、GC

JMM 是"规则",JVM 内存结构是"物理布局"。

JMM:主内存 ←→ 工作内存(抽象)
JVM:堆、栈、方法区、程序计数器(具体)

Q5:JMM 中的八种原子操作是什么?

答: JMM 定义了 8 种操作,用于主内存和工作内存交互:

操作

作用

lock

锁定主内存变量

unlock

解锁主内存变量

read

从主内存读取到工作内存

load

把 read 的值放入工作内存副本

use

把工作内存值传给执行引擎

assign

把执行引擎结果赋给工作内存

store

把工作内存值传到主内存

write

把 store 的值写入主内存

流程:

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 条:

规则

说明

程序顺序规则

同一线程,前操作 hb 后操作

监视器锁规则

解锁 hb 后续加锁

volatile 规则

volatile 写 hb 后续读

线程启动规则

start() hb 子线程所有操作

线程终止规则

线程所有操作 hb join() 返回

线程中断规则

interrupt() hb 检测到中断

对象终结规则

构造函数结束 hb finalize()

传递性

A hb B,B hb C → A hb C

// 程序顺序
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 C

Q8:程序顺序规则一定保证代码按顺序执行吗?

答: 不一定。程序顺序规则只保证 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
}

推导:

  1. data = 1 hb flag = true(程序顺序)

  2. flag = true hb if(flag)(volatile 规则)

  3. 传递性:data = 1 hb int 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

范围

单线程

多线程

目的

保证单线程结果不变

保证多线程可见性

重排

允许,只要结果不变

约束特定操作顺序

层次

编译器/CPU 优化

JMM 规范

关系

允许重排

限制重排

关系:

  • 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;
    }
}

分析:

  1. 线程 A 创建对象,instance = new Singleton()

  2. volatile 写 hb 后续 volatile 读

  3. 线程 B 读 instance,一定看到完整初始化

不加 volatile 的问题:

  • new 分三步:分配内存、初始化、赋值引用

  • 2 和 3 可能重排

  • 线程 B 可能拿到半初始化对象


三、内存屏障

Q15:什么是内存屏障?

答: 内存屏障(Memory Barrier)是 CPU 指令,用于:

  1. 禁止特定指令重排

  2. 强制刷新缓存,保证可见性

本质:在指令之间插入"栅栏",禁止跨越。

// 伪代码
store(x);
StoreLoadBarrier(); // 屏障
load(y);

Q16:内存屏障有哪几种?

答: 四种基本类型:

屏障

作用

示例

LoadLoad

禁止读读重排

Load1; LoadLoad; Load2

StoreStore

禁止写写重排

Store1; StoreStore; Store2

LoadStore

禁止读写重排

Load1; LoadStore; Store2

StoreLoad

禁止写读重排

Store1; StoreLoad; Load2

记忆:

  • 前一个操作 + 后一个操作

  • 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 通过内存屏障实现三大语义:

语义

屏障

可见性

写后刷主内存 + 读前重新加载

有序性

禁止重排

原子性

不保证(仅 64 位读写原子)

// volatile 写的底层
mov [v], 1
lock addl $0, (%rsp)  // StoreLoad 屏障

lock 前缀指令:

  1. 刷新缓存

  2. 使其他 CPU 缓存失效

  3. 禁止重排


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

mfence、lfence、sfence

ARM

dmb、dsb、isb

PowerPC

sync、lwsync、isync

x86 特点:

  • 强内存模型,大部分屏障是空操作

  • 只需 StoreLoad 显式指令(mfence)

  • 用 lock 前缀指令也可实现

// x86 上 volatile 写编译后
mov [v], 1
lock addl $0, (%rsp)  // 相当于 mfence

Q23:内存屏障和缓存一致性协议的关系?

答:

  • 缓存一致性协议(MESI):保证多核缓存数据一致

  • 内存屏障:保证指令顺序和可见性

配合:

  1. 写 volatile:lock 指令让其他缓存失效

  2. 其他 CPU 发现缓存失效,重新从主内存加载

  3. 内存屏障保证指令不重排

CPU A 写 → lock → 缓存失效 → CPU B 重新加载

MESI 状态:

  • Modified:已修改

  • Exclusive:独占

  • Shared:共享

  • Invalid:失效


Q24:内存屏障会影响性能吗?

答: 会,但影响有限:

屏障

性能影响

LoadLoad

几乎无

StoreStore

几乎无

LoadStore

几乎无

StoreLoad

有,需刷写缓冲

优化:

  • x86 上只有 StoreLoad 有开销

  • 读多写少场景,volatile 影响小

  • 高并发写场景,考虑减少 volatile 写

结论:volatile 是轻量级同步,性能远好于锁。


四、综合与实战

Q25:JMM 如何保证可见性?

答:

方式

原理

volatile

写刷主内存,读重新加载

synchronized

加锁清空,解锁刷新

final

构造完成后安全发布

线程启动

start 前操作对子线程可见

线程终止

线程操作对 join 可见

volatile int x = 1;  // 写刷主内存
int y = x;           // 读从主内存加载

Q26:JMM 如何保证有序性?

答:

方式

原理

volatile

内存屏障禁止重排

synchronized

Monitor 进入/退出屏障

happens-before

规则约束

final

禁止构造外重排

// volatile 禁止重排
int a = 1;          // 1
volatile int b = 2; // 2
// 1 不会重排到 2 之后

// synchronized 保证临界区有序
synchronized (lock) {
    // 代码按顺序执行
}

Q27:JMM 如何保证原子性?

答:

方式

原理

synchronized

互斥锁

Lock

AQS + CAS

原子类

CAS

volatile

仅 64 位读写原子

// 非原子
count++;

// 原子
synchronized (lock) { count++; }
atomicInteger.incrementAndGet();

注意:JMM 不保证普通变量的复合操作原子性。


Q28:为什么说 JMM 是"抽象"的?

答: 因为 JMM 不规定具体实现,只定义规则:

  • 主内存/工作内存是抽象概念

  • 不同硬件有不同实现

  • JMM 屏蔽差异,保证跨平台一致

// 同一段代码,不同 CPU 架构
// JMM 保证行为一致
volatile int x = 1;

类比:JMM 像"接口",硬件实现像"实现类"。


Q29:JMM 和硬件内存模型有什么关系?

答:

模型

特点

x86

强内存模型,重排少

ARM

弱内存模型,重排多

JMM

抽象层,屏蔽差异

关系:

  • JMM 建立在硬件模型之上

  • 通过内存屏障适配不同硬件

  • 强模型(x86)屏障开销小

  • 弱模型(ARM)屏障开销大


Q30:如何用 JMM 解释 volatile 可见性?

答: 完整推导:

  1. 线程 A 写 volatile:

    • 把工作内存的值刷到主内存

    • 插入 StoreLoad 屏障

  2. 线程 B 读 volatile:

    • 清空工作内存中该变量的副本

    • 从主内存重新加载

    • 插入 LoadLoad 屏障

  3. 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:如何避免数据竞争?

答:

  1. 使用 volatile(简单标志)

  2. 使用 synchronized(临界区)

  3. 使用 Lock(高级控制)

  4. 使用原子类(简单计数)

  5. 不可变对象(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
}

安全发布方式:

  1. final 字段

  2. volatile 引用

  3. synchronized

  4. 静态初始化器

// 安全发布
private static volatile Holder holder;

Q35:JMM 和 JSR-133 有什么关系?

答: JSR-133 是 JMM 的修订规范(Java 5 引入):

  • 修复了旧 JMM 的缺陷

  • 引入 happens-before 原则

  • 明确 volatile 语义

  • 增强 final 语义

核心改进:

  1. volatile 写 hb 读

  2. final 字段安全发布

  3. 禁止 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 字段的可见性?

答: 通过两条规则:

  1. 写 final:禁止重排到构造外

构造内所有写 → StoreStore → final 写 → 构造结束
  1. 读 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 的未来发展趋势?

答:

  1. VarHandle(JDK 9):更灵活的内存访问

  2. Foreign Memory API:堆外内存

  3. Loom 项目:虚拟线程

  4. 值类型:更高效的内存布局

// VarHandle 示例
VarHandle vh = MethodHandles.lookup()
    .findVarHandle(MyClass.class, "x", int.class);
vh.setVolatile(obj, 1);

核心:JMM 持续演进,但 happens-before 原则不变。

评论