1. JMM 是什么
JMM 全称是 Java Memory Model ,中文叫 Java 内存模型。
它不是在讲某一块真实硬件内存,也不是直接在讲 CPU 的 L1、L2、L3 缓存。
它是一套 Java 多线程规范,用来规定:
text
1. 一个线程修改变量后,其他线程什么时候能看到?
2. 编译器和 CPU 能不能调整代码执行顺序?
3. 哪些操作天然安全,哪些操作需要 volatile、锁或 Atomic 类?
一句话:
JMM 是 Java 对多线程读写共享变量的一套抽象规则。
2. 为什么需要 JMM
Java 程序运行时会有很多优化:
- 编译器可能重排序指令;
- JIT 可能把变量缓存到寄存器;
- CPU 会用 L1、L2、L3 缓存;
- CPU 也可能乱序执行;
- 一个线程改了变量,另一个线程不一定马上看到。
例如:
java
class Demo {
private boolean running = true;
public void loop() {
while (running) {
// do something
}
}
public void stop() {
running = false;
}
}
你以为一个线程调用 stop() 后,另一个线程里的 loop() 会退出。
但如果 running 不是 volatile,循环线程可能一直读到旧值 true,导致停不下来。
这就是可见性问题。
3. JMM 的两个抽象概念
JMM 抽象出两个概念:
text
主内存 Main Memory
工作内存 Working Memory
可以画成这样:
text
主内存
所有线程共享变量的抽象存储区
/ \
/ \
工作内存 A 工作内存 B
Thread A Thread B
线程操作共享变量时,抽象流程是:
text
1. 从主内存读取变量
2. 拷贝到自己的工作内存
3. 在线程中使用、修改
4. 再写回主内存
注意:
主内存和工作内存都是 JMM 的抽象,不是硬件里固定的某一块内存。
4. 主内存是不是物理内存 RAM?
不是完全等于。
可以粗略理解成:
text
主内存 ≈ Java 中所有线程共享变量的最终存储位置
但 JMM 不关心变量实际在 RAM、CPU Cache 还是寄存器里。
JMM 只关心:
text
线程 A 写了变量,线程 B 什么时候能看到?
5. 工作内存是不是 L1、L2、L3?
不是。
工作内存是 Java 层面的抽象,不等于某一级 CPU 缓存。
它底层可能对应:
text
CPU 寄存器
L1 Cache
L2 Cache
L3 Cache
Store Buffer 写缓冲
Load Buffer 读缓冲
JIT 优化出来的临时副本
线程栈上的局部副本
所以不能说:
text
工作内存 = L1
工作内存 = L2
工作内存 = L3
更准确地说:
工作内存代表线程可能持有的共享变量副本,底层可能落在寄存器、CPU 缓存、写缓冲区等地方。
6. CPU 缓存大概是什么
硬件上通常有:
text
寄存器:最快,容量最小
L1 Cache:通常每个 CPU 核心私有
L2 Cache:通常每个 CPU 核心私有,容量比 L1 大
L3 Cache:通常多个核心共享
RAM:物理内存,最慢
大致访问层级:
text
寄存器 -> L1 -> L2 -> L3 -> RAM
但 JMM 不直接规定使用哪一级缓存。
7. volatile 是什么
volatile 是 Java 的关键字,主要保证两件事:
text
1. 可见性
2. 禁止特定重排序
示例:
java
class Demo {
private volatile boolean running = true;
public void loop() {
while (running) {
// do something
}
}
public void stop() {
running = false;
}
}
加了 volatile 后,一个线程执行:
java
running = false;
另一个线程就能看到这个变化。
8. volatile 是不是让线程不用缓存?
不是。
这个说法不准确:
text
volatile 让线程不用自己的缓存,直接读主存
更准确的说法是:
volatile 通过内存屏障和 CPU 缓存一致性机制,保证变量读写的可见性和有序性,但不会真正关闭 CPU 缓存。
CPU 仍然会使用 L1、L2、L3。
9. volatile 的可见性
java
private volatile int flag = 0;
线程 A:
java
flag = 1;
线程 B:
java
int x = flag;
JMM 保证:
text
线程 B 能看到线程 A 对 flag 的写入。
更专业地说:
对 volatile 变量的写,happens-before 后续任意线程对同一个 volatile 变量的读。
10. volatile 的有序性
示例:
java
class Demo {
private int data = 0;
private volatile boolean ready = false;
public void writer() {
data = 100;
ready = true;
}
public void reader() {
if (ready) {
System.out.println(data);
}
}
}
如果 reader 线程看到:
java
ready == true
那么它也能看到:
java
data == 100
原因是:
text
data = 100 不能被重排序到 ready = true 之后
ready = true 是 volatile 写
reader 读到 ready = true 后,可以看到 volatile 写之前的普通写
11. volatile 不能保证原子性
volatile 不能保证复合操作原子性。
java
volatile int count = 0;
count++;
count++ 实际是三步:
text
1. 读 count
2. 加 1
3. 写回 count
多个线程同时执行,还是可能丢数据。
正确方式:
java
AtomicInteger count = new AtomicInteger();
count.incrementAndGet();
或者:
java
synchronized
12. happens-before 是 JMM 的核心
JMM 最重要的概念是:
text
happens-before
如果操作 A happens-before 操作 B,那么:
text
1. A 的结果对 B 可见
2. A 的执行顺序排在 B 之前
13. 常见 happens-before 规则
13.1 程序顺序规则
同一个线程内,前面的操作 happens-before 后面的操作。
java
int a = 1;
int b = 2;
在当前线程里:
text
a = 1 happens-before b = 2
13.2 volatile 规则
对 volatile 变量的写,happens-before 后续对这个变量的读。
13.3 锁规则
一个线程释放锁,happens-before 另一个线程获取同一把锁。
java
synchronized (lock) {
value = 10;
}
13.4 线程启动规则
调用 Thread.start() 前的操作,对新线程可见。
13.5 线程 join 规则
线程中的操作 happens-before 其他线程从 join() 返回。
14. synchronized 和 volatile 的区别
| 能力 | volatile | synchronized |
|---|---|---|
| 可见性 | 有 | 有 |
| 有序性 | 有 | 有 |
| 原子性 | 不保证复合操作 | 保证同步块互斥 |
| 是否阻塞 | 不阻塞 | 可能阻塞 |
| 适合场景 | 状态标志、单次发布 | 多变量一致性、临界区 |
15. AtomicInteger 和 volatile 的区别
java
volatile int count;
只能保证读写可见,不能保证:
java
count++
线程安全。
java
AtomicInteger count = new AtomicInteger();
可以保证:
java
count.incrementAndGet();
是原子操作。
16. 双重检查锁为什么要 volatile
经典单例:
java
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;
}
}
为什么 instance 要加 volatile?
因为:
java
instance = new Singleton();
可能被拆成:
text
1. 分配内存
2. 初始化对象
3. 把对象地址赋给 instance
如果发生重排序,可能变成:
text
1. 分配内存
2. 把对象地址赋给 instance
3. 初始化对象
另一个线程看到 instance != null,但对象还没初始化完。
volatile 可以禁止这种危险重排序。
17. 常见误区
误区 1:volatile 会禁用 CPU 缓存
错误。
volatile 不会禁用 CPU 缓存。
它是通过内存屏障和缓存一致性协议保证可见性。
误区 2:工作内存就是 L2 或 L3
错误。
JMM 的工作内存是抽象概念,可能对应寄存器、L1、L2、L3、写缓冲、JIT 临时副本等。
误区 3:volatile 可以替代 synchronized
不完全可以。
volatile 不能保证复合操作原子性,也不能保护多个变量的一致性。
误区 4:volatile 修饰集合就线程安全
错误。
java
volatile List<String> list = new ArrayList<>();
这里只保证 list 这个引用的可见性,不保证 ArrayList 内部操作线程安全。
18. 速查表
| 场景 | 推荐方式 |
|---|---|
| 一个线程通知另一个线程停止 | volatile boolean |
| 简单状态发布 | volatile |
| 自增计数 | AtomicInteger / LongAdder |
| 多变量一致更新 | synchronized / Lock |
| 保护临界区 | synchronized / Lock |
| 高并发计数 | LongAdder |
| 单例双重检查 | volatile + synchronized |
19. 总结
JMM 是 Java 多线程的内存可见性规则。
它抽象出:
text
主内存
工作内存
但这两个概念不等于具体硬件:
text
主内存 ≠ 单纯物理 RAM
工作内存 ≠ 固定的 L1/L2/L3
volatile 的作用是:
text
1. 保证可见性
2. 禁止特定重排序
但它不能保证:
text
复合操作原子性
集合线程安全
多个变量的一致性
一句话:
volatile 不是让线程不用缓存,而是让 JVM 和 CPU 按照 JMM 的规则保证变量读写的可见性和有序性。