JMM
JMM是什么?
Java内存模型(Java Memory Model, JMM)是一个抽象的概念和规范。它并非真实存在的物理内存划分,而是Java虚拟机(JVM)定义的一套规则,用来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果
简单来说,JMM定义了线程和主内存之间的抽象关系以及线程之间共享变量的可见性、有序性和原子性的规则
为什么需要JMM?
在多线程中,每个线程运行时可能会把共享变量复制到线程工作内存(CPU缓存、寄存器) 中,而不是直接操作主内存。
主内存与工作内存
主内存:
- 这是一个所有线程共享的区域
- 所有共享变量(实例字段、静态字段、构成数组对象的元素)都存储在主内存中
- 注意:这里的"主内存"与JVM内存区域中的"Java堆"或"方法区"在概念上有所关联但并不等同。可以理解为,堆和方法区中存储的共享数据,逻辑上都属于JMM的主内存范畴
工作内存:
- 这是每个线程私有的区域
- 线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,不能直接读写主内存中的变量
- 工作内存中存储的是主内存中共享变量的副本
- 重要细节 :工作内存是一个抽象概念,它涵盖了CPU高速缓存、寄存器、写缓冲区等,并非一块真实的内存区域
线程间通信过程:
线程A和线程B如果要通信(例如A修改一个共享变量,B来读取),必须通过主内存作为中介
线程A将更新后的共享变量值从自己的工作内存 刷新(flush) 到主内存
线程B从主内存中 读取(read) 该共享变量的最新值到自己的工作内存
这个模型清晰地揭示了可见性问题的根源:如果线程A修改了变量但没有及时刷新到主内存,或者线程B没有及时从主内存更新,那么B看到的就是旧值


JMM存在的必要性
由于JVM运行时的实体是线程,每一个线程运行时都会创建一个工作内存,作为线程的私有空间,每个线程操作变量都在自己的工作内存,从主内存copy一份数据到工作内存,在工作内存中进行操作后,再将其刷新回主内存。如果是在多线程的情况下,则会出现线程安全问题。因此JMM规范规定了如下八种操作来完成数据的操作:
(1) lock(锁定) :作用于主内存的变量,把一个变量标记为一条线程独占状态
(2)unlock(解锁) :作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
(3)read(读取) :作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
(4)load(载入) :作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
(5)use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
(6)assign(赋值) :作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量
(7)store(存储) :作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作
(8)write(写入) :作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中

共同定义了变量如何从主内存拷贝到工作内存、以及如何从工作内存同步回主内存的底层实现细节和规则。
它们确保了volatile
等关键字能够实现其承诺的内存可见性效果。
例如,对一个volatile
变量的写操作,会强制触发store
和write
操作,立即将新值同步到主内存 ;对volatile变量的读操作,会强制触发read
和load
操作,从主内存获取最新值
JMM与JVM内存区域的区别
特性 | Java内存模型 (JMM) | JVM内存区域 (Runtime Data Areas) |
---|---|---|
层面/范畴 | 并发编程规范,是语言层面的抽象模型 | JVM运行时内存布局,是JVM管理的物理或逻辑分区 |
目的 | 解决多线程环境下的可见性、有序性、原子性问题,屏蔽底层硬件差异 | 定义程序在运行时,数据存储在哪里的问题 |
核心概念 | 主内存、工作内存、happens-before原则、重排序、内存屏障 | 堆(Heap)、虚拟机栈(VM Stack)、方法区(Method Area)、程序计数器、本地方法栈 |
关系 | JMM决定了如何在这些内存区域中安全地读写共享变量 | JVM内存区域是JMM所依赖的数据载体。例如,JMM中的主内存主要对应JVM内存区域的堆和方法区 |
Volatitle
什么是volatile
volatile
可以看作是轻量级的synchronized
,他只保证了共享变量 的可见性,是java虚拟机提供的轻量级的同步机制。在线程A修改被volatile
修饰的共享变量之后,线程B能够读取到正确的值。Java在多线程中操作共享变量的过程中,会存在指令重排序与共享变量工作内存缓存的问题。volatile
有三大特性:可见性、不保证原子性、禁止指令重排。
即:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
1.实现可见性
volatile
是 Java 中的一个关键字,用来保证变量的可见性,也就是确保一个线程对某个共享变量的修改,其他线程能够立刻看到。
内存屏障
volatile
通过 内存屏障(Memory Barrier) 来保证可见性。当线程修改一个 volatile
变量时,Java 虚拟机会在写入 volatile
变量前后插入内存屏障,确保以下两点
- 写操作的可见性 :线程对
volatile
变量的写操作,立即刷新到主内存。 - 读操作的可见性 :线程对
volatile
变量的读操作,从主内存读取最新值。
具体来说:
- 写操作 :当一个线程写入
volatile
变量时,JVM 会确保将变量的值写入 主内存。 - 读操作 :当一个线程读取
volatile
变量时,JVM 会从 主内存 读取该值,而不是从本地缓存中读取。
解决了不可见性的问题。
2.非原子性
每次都会不一样。这是为什么呢?
通过tager包下 输入cmd在控制命令中反序列化查看。
javap -c VolatileTest.class
你会发现 里边的 num++
不是一行操作,所以会导致非原则性问题。

解决原子性问题呢?
1.使用synchronized
或lock

2.使用JUC提供的Auto原子性的类

为什么能提供原子性呢?---因为它和底层操作系统挂钩 CAS
查看源码可以看到都是native
相关的,本地方法接口

3.禁止指令重排
什么是指令重排(Instruction Reordering)
指令重排 是 CPU 或编译器为提高执行效率而对指令执行顺序做的调整。
例如:
ini
int a = 1; // ①
int b = 2; // ②
a = a + 5; // ③
b = a * 2; // ④
在单线程下,编译器可能将顺序优化为:
①②③④ → ②①③④
因为这样执行更快,而且结果对单线程没有影响。
但如果是在多线程下
当线程间共享变量时,指令重排可能导致可见性问题和乱序执行问题。
ini
boolean flag = false;
int num = 0;
void writer() {
num = 1; // A
flag = true; // B
}
void reader() {
if (flag) { // C
System.out.println(num); // D
}
}
理论上应该输出 1
,
但由于可能发生 指令重排(B 和 A 交换),读线程可能看到:
ini
flag = true, num = 0
JMM 中的「重排序」有三种类型
类型 | 说明 |
---|---|
1️⃣ 编译器优化重排 | 编译阶段重排指令 |
2️⃣ CPU 指令重排 | CPU 执行时乱序执行 |
3️⃣ 内存系统重排 | 缓存与主内存同步延迟 |
内存屏障
为了防止这些重排问题,JMM 在必要时会插入内存屏障指令
在volatile
中会自动加上内存屏障
操作 | 内存屏障效果 |
---|---|
读(load)volatile变量 | 在读前插入 LoadLoad + LoadStore 屏障,禁止前面的读/写重排到后面 |
写(store)volatile变量 | 在写后插入 StoreLoad + StoreStore 屏障,禁止后面的读/写重排到前面 |

什么时候使用内存屏障比较多? ----单例模式下