1.初识JMM
虽然“JMM”这个名字听起来像是“JVM”的兄弟,实际上两者是完全不同的体系,职责也截然不同。
-
JVM(Java Virtual Machine):它是一台虚拟的“执行机器”,负责解释执行 Java 字节码,让 Java 程序能在各个平台运行。
-
JMM(Java Memory Model,Java 内存模型):它并不是一个具体的组件,而是一套规范和规则,规定了多线程环境下变量的读写规则、可见性、有序性、原子性等问题。JMM 用于协调线程之间如何安全地共享数据。
JMM 解决的是一个很实际的问题:多个线程到底是怎么“看见”共享变量的?
在没有 JMM 统一规范之前,不同平台、不同 CPU、不同编译器可能在处理线程之间的共享数据时表现不一致,容易出现千奇百怪的并发 bug。因此 JMM 的目标就是——
“让 Java 程序在任何平台、任何硬件下运行时,多线程的行为都是一致且可预期的。”
2.JMM结构
Java内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。局部变量和方法参数直接存储在线程私有的栈帧中(属于工作内存的一部分),它们天生就是线程隔离的,不存在“从主内存拷贝”的过程,因此也不存在多线程可见性问题。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行。线程不能直接读写主内存中的变量。


小知识点:
1.CPU先从Cache中查找数据,如果找不到,再去主存(Memory)中取,最后才去硬盘。Cache的作用是加速数据访问,减少CPU等待时间。如果CPU频繁访问某些数据,使用Cache可以提高效率。
2.高速缓存Cache它不是寄存器,而是一种介于CPU和内存之间的高速存储器。
3.当你运行一个程序时,操作系统会把它从硬盘(如C盘或D盘)加载到内存(Memory)中,这样CPU才能快速访问。
1.可见性:
一个线程对共享变量值的修改,能够被其他线程及时地看到。
由于每个线程都有自己的工作内存,这些工作内存中存储的相当于同一个共享变量的一份副本。因此,线程1修改自己的工作内存后,线程2的工作内存不一定会察觉到这一修改。

假设主内存中某个变量 a = 10,当线程1 和线程2 启动后,它们各自从主内存中读取了 a 的值到自己的工作内存中。此时线程1 的工作内存和线程2 的工作内存里都保存着 a = 10。
接着,如果线程1 将自己工作内存中的 a 修改为 20,由于线程1 修改的是自己本地的副本,并不会立刻把结果写回主内存,所以:
线程2 完全感知不到线程1 已经修改了
a的值,它仍然看到的是a = 10。
这就造成了所谓的 “内存不可见性”问题:一个线程对共享变量的修改,对另一个线程来说可能是不可见的。
3.volatile如何解决内存可见性问题
volatile 保证可见性,靠的是底层 JIT 编译器在写操作中生成带 lock 前缀的汇编指令,这个指令通过缓存一致性协议,确保变量修改对所有 CPU 可见。
加 volatile 之后发生了什么?
当你加上:
private static volatile boolean isRunning = true;
当一个变量被声明为 volatile 时,Java 会保证:
-
每次读取该变量时,都直接从主内存加载最新值,不使用寄存器或线程本地缓存的旧数据;
-
每次写入该变量时,都会立即刷新到主内存,确保其他线程能够看到最新值;
-
同时,JVM 会在读写
volatile变量的操作前后插入特殊的“内存屏障”指令,防止编译器和处理器对这些操作进行重排序;
这意味着线程在访问 volatile 变量时,能获得最新的值,并且程序执行顺序不会因为优化被打乱。
因此,在你的循环中,线程1会每次都从主内存读取最新的 isRunning 值,当线程2把它改成 false,线程1就能立即看到,从而跳出循环。
总结
1. 可见性 当一个线程修改了volatile变量的值,对于其他线程新值是立即可见的
2.有序性 精致指令重排序优化
3.不保证原子性: volatile不保证 复合操作 比如i++的原子性
4.指令重排序
在上面我们分析了,为什么加上了volatile关键字,就能确保各个线程都能看到共享变量,其中一个重要的原因是:JVM为变量保驾护航,防止指令重排序 ,究竟什么是指令重排序呢?

水果摊的分布,可能和我们水果清单的顺序不一致,如果强行按照初始的水果清单来购买,肯定会重跑很多次,如果能调整一下购买的顺序,按照图示的顺序就会轻松很多。
指令重排序就是编译器或处理器为了提升程序执行效率,在不影响单线程程序语义的前提下,调整指令执行的顺序。
这种优化让CPU更好地利用流水线、缓存和执行单元,提高性能。
如果没有 volatile,这种重排序在多线程环境下可能导致某些操作看似乱序执行,引发难以发现的并发问题。
而加上 volatile 后,JVM 会插入必要的内存屏障,禁止某些关键指令被重排序,保证共享变量的写入对其他线程是可见的,避免并发混乱。
4.1 new 的底层过程
在 Java 中,执行 new Object() 实际上大致分为三个步骤(JMM 视角):
-
分配内存空间
-
调用构造器,初始化对象
-
返回对象引用
4.2 问题:重排序可能打乱顺序
编译器或 CPU 可能会把 步骤 2 和步骤 3 重排序:
-
正常顺序(没重排序): 1 → 2 → 3
-
内存分配完成,对象初始化完毕,再把引用返回。
-
-
可能的重排序顺序: 1 → 3 → 2
-
先返回引用(指向一块尚未初始化完成的内存),然后才初始化对象。
-
为什么CPU要冒着风险去进行1-3-2这样的重排序?
计算机执行指令时,最重要的目标就是 最大化流水线和缓存利用率。 在 new 的三个步骤里:
-
分配内存(很快)
-
调用构造方法,初始化对象(可能比较慢,要执行很多赋值/逻辑)
-
把引用赋值给变量(只是一次简单的赋值,很快)
对 CPU 来说,如果按照 1 → 2 → 3 执行:
-
它必须等 初始化(步骤 2)完成 才能执行 步骤 3。
-
这中间 CPU 的流水线、寄存器可能就会“闲着”。
而如果 允许 1 → 3 → 2:
-
分配内存后,就可以立刻把引用存到变量里(很快的操作)。
-
然后慢慢去做初始化(步骤 2)。
-
CPU 没有“等待”,流水线利用率更高,程序整体可能更快。
4.3 程序举例(经典 DCL 单例)
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 可能被重排序
}
}
}
return instance;
}
}
4.3.1 可能的问题:
-
假设线程 A 执行
instance = new Singleton(); -
如果发生重排序,步骤可能是:
-
A 分配内存
-
A 把引用赋值给 instance(但对象还没初始化完!)
-
A 开始执行构造方法
-
-
同时线程 B 进入
getInstance(),发现instance != null,直接返回。 -
但这时
instance可能还是“半初始化状态”,使用它可能抛出异常。
4.3.2 解决方案:加 volatile
private static volatile Singleton instance;
-
volatile禁止了 步骤 2 和步骤 3 的指令重排序。 -
保证一个线程看到
instance != null时,对象一定已经初始化完成。


被折叠的 条评论
为什么被折叠?



