1. 先搞懂:为什么需要 Java 内存模型(JMM)?
首先明确核心目的:JMM 是一套抽象的内存访问规范(不是实际的物理内存),解决多线程环境下的「可见性、原子性、有序性」问题,让 Java 程序在不同硬件/操作系统下有一致的内存访问行为。
可以用一个比喻理解:
- 每个线程就像一个“办公室员工”,有自己的“草稿纸”(线程私有工作内存);
- 所有线程共享一个“公共文件柜”(主内存),存放共享变量;
- 线程操作共享变量时,不能直接操作文件柜,必须先把变量拷贝到自己的草稿纸(工作内存),修改后再写回文件柜;
- JMM 就是规定了“草稿纸和文件柜之间的数据交互规则”,避免多个员工同时改文件导致数据混乱。
2. JMM 的核心概念与结构
JMM 定义了线程、工作内存、主内存三者的交互关系:
- 主内存:存储所有共享变量(实例变量、静态变量),是所有线程可见的公共区域;
- 工作内存:每个线程独有的内存区域,存储共享变量的副本,线程对变量的所有操作(读/写)都必须在工作内存中进行,不能直接操作主内存;
- 交互规则:JMM 定义了 8 种原子操作(lock/unlock、read/load、use/assign、store/write),规范了工作内存和主内存之间的数据传递流程(比如 read 是把主内存变量读到工作内存,write 是把工作内存变量写回主内存)。
3. JMM 解决的三大核心问题
这是 JMM 的核心价值,也是多线程并发的核心痛点:
(1)可见性:一个线程修改的变量,其他线程能立刻看到
问题场景:线程 A 修改了共享变量 flag,但只存在于 A 的工作内存,线程 B 还在读取自己工作内存中旧的 flag 值,导致逻辑错误。
// 可见性问题示例(不加 volatile 会导致线程 B 一直循环)
public class VisibilityDemo {
private static boolean flag = false; // 共享变量,无 volatile
public static void main(String[] args) throws InterruptedException {
// 线程 B:循环读取 flag
new Thread(() -> {
while (!flag) {
// 空循环
}
System.out.println("线程 B 退出循环");
}).start();
Thread.sleep(1000);
// 线程 A:修改 flag 为 true
new Thread(() -> {
flag = true;
System.out.println("线程 A 修改 flag 为 true");
}).start();
}
}
问题原因:线程 A 修改 flag 后,未及时写回主内存;即使写回,线程 B 也未从主内存重新读取,仍用工作内存的旧值。
JMM 的解决方案:
volatile关键字:强制变量的读写直接操作主内存,禁止工作内存缓存,保证修改对其他线程立即可见;synchronized/Lock:加锁时会清空工作内存的旧值,解锁时会把工作内存的修改写回主内存,间接保证可见性。
修复后的代码:
private static volatile boolean flag = false; // 加 volatile 保证可见性
(2)原子性:操作要么全部执行,要么全部不执行
问题场景:多线程对 count 进行自增(count++),该操作拆分为「读-改-写」三步,非原子操作,会导致最终结果小于预期。
// 原子性问题示例
public class AtomicityDemo {
private static int count = 0;
public static void main(String[] args) throws InterruptedException {
// 1000 个线程,每个线程执行 1000 次 count++
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
count++;
}
}).start();
}
Thread.sleep(2000);
System.out.println("最终 count 值:" + count); // 结果远小于 1000000
}
}
问题原因:线程 A 读取 count=10,还没修改,线程 B 也读取 count=10,两者都加 1 后写回,最终 count=11(本该是 12)。
JMM 的解决方案:
synchronized/Lock:保证同一时间只有一个线程执行原子操作;java.util.concurrent.atomic包(AtomicInteger/AtomicLong):基于 CAS 操作实现原子性,效率更高。
修复后的代码:
private static AtomicInteger count = new AtomicInteger(0);
// 替换 count++ 为原子操作
count.incrementAndGet();
(3)有序性:程序执行顺序与代码书写顺序一致
问题场景:JVM 为了优化性能,会对指令进行「重排序」(编译器重排序、CPU 重排序),单线程下不影响结果,但多线程下会导致逻辑混乱。
// 有序性问题示例(指令重排序导致线程 B 可能读到 a=0, b=1)
public class OrderingDemo {
private static int a = 0;
private static boolean b = false;
public static void main(String[] args) throws InterruptedException {
// 线程 A:先修改变量,再改标志位
new Thread(() -> {
a = 1; // 指令1
b = true; // 指令2(可能被重排序到指令1 之前)
}).start();
// 线程 B:根据标志位读取变量
new Thread(() -> {
if (b) { // 指令3
System.out.println("a 的值:" + a); // 可能输出 0
}
}).start();
}
}
问题原因:JVM 可能将线程 A 的指令重排序为「先执行 b=true,再执行 a=1」,导致线程 B 读到 b=true 但 a=0。
JMM 的解决方案:
volatile关键字:禁止指令重排序(除了保证可见性,还能保证有序性);synchronized/Lock:保证同一时刻只有一个线程执行代码块,间接禁止重排序;final关键字:被 final 修饰的变量,初始化完成后不会被重排序。
修复后的代码:
private static volatile int a = 0;
private static volatile boolean b = false;
4. JMM 的核心规则:happens-before(先行发生)
JMM 用「happens-before」规则定义操作之间的先后顺序,只要满足该规则,就保证可见性和有序性,无需额外同步。核心规则包括:
- 程序次序规则:单线程内,前面的操作 happens-before 后面的操作;
- volatile 变量规则:对 volatile 变量的写操作 happens-before 后续对该变量的读操作;
- 锁规则:解锁操作 happens-before 后续对同一把锁的加锁操作;
- 线程启动规则:Thread.start() 操作 happens-before 线程内的所有操作;
- 线程终止规则:线程内的所有操作 happens-before 线程的终止检测(如 Thread.join())。
举个例子:
volatile int x = 0;
// 线程 A
x = 1; // 写操作
// 线程 B
int y = x; // 读操作
根据「volatile 变量规则」,线程 A 的写操作 happens-before 线程 B 的读操作,因此 B 能读到 x=1,保证了可见性和有序性。
总结
- JMM 是抽象规范,核心解决多线程的「可见性、原子性、有序性」问题,定义了线程工作内存和主内存的交互规则;
- 关键解决方案:
- 可见性:
volatile、synchronized、Lock; - 原子性:
synchronized、Lock、Atomic 原子类; - 有序性:
volatile、synchronized、Lock、final;
- 可见性:
- happens-before 规则是 JMM 的核心,满足该规则的操作无需额外同步,就能保证内存可见性和有序性。
转载自CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/u011614717/article/details/158495943



