内存模型
更新: 1/19/2026 字数: 0 字 时长: 0 分钟
Java 内存模型(JMM)是一个抽象概念,它不是实际存在的硬件或软件,而是一套规则和规范。这套规则定义了程序中所有变量(包括实例变量、静态变量和数组元素)在内存中的访问方式,特别是在多线程环境下的行为。
JMM 的作用:
- 平台一致性:屏蔽不同硬件和操作系统内存访问的差异,确保 Java 程序在任何平台下都能有一致的并发行为。
- 定义内存关系:规定了线程如何与内存进行交互。
核心思想:
- JMM 假定系统存在一个主内存(Main Memory),所有线程共享的变量都存储在这里。
- 每条线程都有自己的工作内存(Working Memory),里面保存着主内存中这些共享变量的副本。
- 线程对变量的所有操作(读、写)都必须先在自己的工作内存中进行,不能直接操作主内存。
- 线程之间的通信(数据传递)也必须通过主内存来完成。

主内存与工作内存的对应关系(与 JVM 内存区域的区别):JMM 的主内存和工作内存是抽象概念,与 JVM 运行时数据区(堆、栈、方法区等)不是直接对应的。
- 主内存:更接近于物理内存,主要存储 Java 堆中的对象实例数据。
- 工作内存:更接近于 CPU 的高速缓存和寄存器,存储线程私有的变量副本。
内存交互
JMM 定义了 8 种原子操作来规范主内存和工作内存之间的数据交互。每个操作都是不可中断的。
注意:除了
volatile修饰的long和double类型变量,其他long和double变量的读写操作在 32 位系统上可能不是原子的,而是被拆分成两次 32 位操作。

lock(锁定):作用于主内存,将变量标记为线程独占状态。unlock(解锁):作用于主内存,释放变量的独占状态,允许其他线程锁定。read(读取):作用于主内存,将变量值从主内存传输到线程的工作内存。load(载入):作用于工作内存,紧接在read之后,将read到的值放入工作内存的变量副本。use(使用):作用于工作内存,将工作内存中变量的值传递给执行引擎(CPU),每次使用变量前都需要。assign(赋值):作用于工作内存,将执行引擎计算出的值赋给工作内存的变量。store(存储):作用于工作内存,将工作内存中变量的值传输到主内存。write(写入):作用于主内存,紧接在store之后,将store得到的值写入主内存的变量。
规则总结:这些操作必须遵循严格的顺序和组合规则,以确保内存操作的正确性。例如,
read和load必须成对出现,store和write也必须成对出现。lock和unlock必须成对,且unlock前必须先store和write。lock操作会清空工作内存中该变量的副本,强制线程重新从主内存加载最新值。
参考文章:https://github.com/CyC2018/CS-Notes/blob/master/notes/Java 并发.md
三大特性
可见性
定义:当多个线程访问同一个共享变量时,一个线程修改了这个变量的值,其他线程能够立即看到并读取到这个修改后的最新值。
问题根源:由于每个线程都有自己的工作内存(缓存),它们操作的是共享变量的副本。如果一个线程修改了副本,但没有及时同步回主内存,或者其他线程没有及时从主内存刷新副本,就会导致“不可见”问题,即线程读取到的是旧值。
示例:main 线程对 run 变量的修改对于 t 线程不可见,导致了 t 线程无法停止
static boolean run = true; // 默认值是 true
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
while(run) { // t 线程持续检查 run 变量
// ... 其他操作
}
System.out.println("t 线程停止了。");
});
t.start();
Thread.sleep(1000); // 主线程等待 1 秒
run = false; // 主线程修改 run 的值
System.out.println("主线程将 run 设置为 false。");
// 预期:t 线程应该在 1 秒后停止,但实际可能不会。
}原因分析:
- 初始状态:
t线程启动后,将run变量的初始值true从主内存读取到自己的工作内存中。 - JIT 优化:为了提高效率,JIT 编译器可能会将
while(run)循环优化为直接读取t线程工作内存中的run副本,甚至可能将其视为一个常量true,从而减少频繁访问主内存。 - 修改不同步:1 秒后,
main线程修改了run的值为false,并同步回主内存。 t线程的盲区:由于t线程可能持续从自己的工作内存缓存中读取run的旧值(true),它无法感知main线程对主内存中run变量的修改,导致while(run)循环永远不会结束。

解决方案:
- 使用
volatile关键字修饰run变量:volatile强制所有线程从主内存读取最新值,并立即写回主内存。 - 使用
synchronized或Lock:它们不仅保证原子性,也隐式地保证了可见性(在释放锁前会把工作内存修改同步回主内存,在获取锁后会清空工作内存,强制从主内存读取)。 - 使用
final关键字修饰的变量是不可变的,虽然不是直接解决可见性问题,但其不可变性天然保证了多线程环境下的一致性(因为值不会变)。
原子性
定义:一个或一系列操作是不可分割的,要么全部成功执行,要么全部不执行(失败),在执行过程中不会被任何其他线程的操作打断。它保证了指令的完整性,不会受到线程上下文切换的影响。
JMM 对原子操作的规则:JMM 定义了 8 个内存交互操作,这些操作本身是原子的。但为了保证更高级别操作的原子性,JMM 规定了一些使用规则:
read和load必须成对出现,不能单独执行。store和write也必须成对出现。- 线程不能丢弃
assign操作,即对工作内存变量的修改必须同步回主内存。 - 线程不能无原因地将工作内存的数据同步回主内存(必须是发生了
assign操作)。 - 新变量只能在主内存中创建。线程在使用或存储变量前,必须先进行
assign或load操作(即从主内存初始化)。 - 一个变量在同一时刻只允许一个线程对其执行
lock操作。lock操作可以被同一线程重复执行多次(可重入),但只有执行相同次数的unlock操作后,变量才会被真正解锁。lock和unlock必须成对出现。 - 对变量执行
lock操作会清空工作内存中此变量的值,强制线程在使用前重新从主内存加载。 - 不允许对未被
lock操作锁定的变量执行unlock操作,也不允许解锁被其他线程锁定的变量。 - 对变量执行
unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。
解决方案:
- 使用
synchronized关键字:保证代码块或方法的原子性。 - 使用
java.util.concurrent.atomic包下的原子类:如AtomicInteger、AtomicLong,它们通过 CAS 操作实现原子性。 - 使用
java.util.concurrent.locks.Lock接口的实现类:如ReentrantLock。
有序性
定义:
- 本线程内观察(有序):无论底层指令如何重排序,程序都会保证最终执行结果与你写的代码逻辑一致,让你感觉是按顺序执行的。
- 就像你在家穿衣服,你是先穿袜子还是先穿裤子,你自己并不在意,只要最后你“穿整齐了出门”这个结果是对的,你就觉得自己的逻辑是有序的。
- 线程间观察(无序):在没有同步约束时,一个线程为了性能而产生的指令重排会被另一个线程察觉,从而看到逻辑颠倒或未完成的中间状态。
- 你在窗外看发货员。发货员(线程 A)为了效率,先在系统里点了“已发货”,然后再去打包快递。你(线程 B)看到系统显示“已发货”,兴冲冲去取件,结果发现包裹还是空的。在你的视角里,发货员的操作顺序“乱了”。
指令重排序:为了提高性能,编译器和处理器会对指令进行优化,调整其执行顺序,只要不改变单线程内的执行结果,这种重排序就是允许的。
重排序的阶段:源代码 → 编译器优化的重排 → 指令并行的重排 → 内存系统的重排 → 最终执行指令
- CPU 流水线:现代 CPU 支持多级指令流水线(如经典的“取指令、指令译码、执行指令、访存取数和结果写回”五级流水线)。CPU 可以在一个时钟周期内,同时运行五条指令的不同阶段。这本质上不能缩短单条指令的执行时间,但通过并行处理提高了指令的吞吐率。
- 数据依赖性:处理器在进行重排序时,必须考虑指令之间的数据依赖性。如果一个指令的执行结果是另一个指令的输入,那么这两个指令不能被重排序。
- 单线程环境:即使存在指令重排,由于数据依赖性的保证,最终执行结果和代码顺序的结果是一致的。
- 多线程环境:线程交替执行时,由于编译器和处理器的优化重排,一个线程可能观察到另一个线程的操作顺序与代码顺序不一致,这可能导致意想不到的错误。
示例:著名的“双重检查锁定(DCL)”单例模式中,如果不使用 volatile,就可能因为指令重排序导致问题。
class Singleton {
private static Singleton instance; // 考虑不使用 volatile 的情况
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
// 这行代码可能发生指令重排序
// 1. memory = allocate() 分配内存
// 2. ctorInstance(memory) 初始化对象
// 3. instance = memory 设置 instance 指向内存
// 如果重排序为 1 -> 3 -> 2,那么在 3 执行后,另一个线程可能看到非空的 instance,
// 但此时对象还未完全初始化,导致访问到半初始化对象。
instance = new Singleton();
}
}
}
return instance;
}
}解决方案:使用 volatile 关键字:volatile 除了保证可见性,还能禁止指令重排序(特别是针对 volatile 变量的读写操作)。在 DCL 中,private static volatile Singleton instance; 可以解决重排序问题。
