聊聊Java的内存模型

重阳,Java 的内存模型(Java Memory Model,简称 JMM)是并发编程里最核心、最容易让人迷惑的概念之一。

很多人会把它和 JVM 运行时数据区(堆、栈、方法区……)混在一起,其实这是两个完全不同的东西:

概念全称关注点主要解决什么问题属于哪个规范
JVM 内存结构JVM Runtime Data Areas线程运行时内存怎么划分对象放哪、局部变量放哪、GC 范围《Java 虚拟机规范》
Java 内存模型 JMMJava Memory Model多线程下共享变量的可见性有序性原子性线程之间变量读写何时可见、能不能重排序《Java 语言规范》JLS 第17章 + JSR-133

今天我们重点聊 JMM(Java 内存模型),也就是多线程并发编程的“内存契约”。

1. 为什么需要 JMM?(最本质的原因)

现代 CPU 和编译器为了性能,会做各种激进优化:

  • 寄存器缓存(线程本地)
  • CPU 缓存(L1/L2/L3)
  • 写缓冲区(Store Buffer)
  • 失效队列(Invalidate Queue)
  • 指令重排序(as-if-serial + 内存乱序)

这些优化在单线程下没问题,但在多线程下会导致“我改了,你看不见”“看到的顺序不对”

举个最经典的例子(很多人面试都见过):

// 线程1
boolean ready = false;
int number = 0;

void writer() {
    number = 42;          // ①
    ready = true;         // ②
}

// 线程2
void reader() {
    if (ready) {          // ③
        System.out.println(number);  // 可能打印 0,而不是 42!
    }
}

可能的结果:线程2 看到 ready=true,却看到 number=0。

原因:编译器/CPU 把①和②重排序了,或者写缓冲区导致可见性延迟。

JMM 的使命:在不牺牲太多性能的前提下,给程序员提供一套可预测的内存可见性规则

2. JMM 的核心抽象(工作内存 vs 主内存)

JMM 把每个线程想象成有自己的工作内存(Working Memory),所有共享变量的真身放在主内存(Main Memory)。

线程对变量的读写操作必须经过下面八个原子操作:

操作含义从哪里到哪里
lock把主内存变量标记为线程独占主内存 → 线程
unlock释放独占标记线程 → 主内存
read从主内存读取变量值主内存 → 工作内存
load把 read 的值放入工作内存的变量副本
use线程使用工作内存中的变量值
assign线程把值赋给工作内存的变量副本
store把工作内存变量值传给主内存工作内存 → 主内存
write把 store 的值写入主内存的变量

真实 JVM 实现不一定严格这样分,但逻辑上必须遵守这个模型。

3. JMM 真正承诺的东西:happens-before 关系(最重要!)

JMM 不承诺“一定顺序执行”,而是承诺:

如果 A happens-before B,那么 A 在内存上做的所有修改,在 B 看来都是可见的。

常见的 happens-before 规则(2026 年仍然是这些,没有大改动):

序号规则名称描述强度
1程序顺序规则单线程内,前面的操作 happens-before 后面的操作
2监视器锁规则unlock → 同一个锁的后续 lock
3volatile 变量规则对 volatile 变量的写 → 后续对同一个变量的读
4线程启动规则Thread.start() → 线程内任意操作
5线程终止规则线程内所有操作 → Thread.join() 返回 / Thread.isAlive()=false
6线程中断规则interrupt() → 检测到中断(抛异常或 isInterrupted()=true)
7对象终结规则对象初始化完成 → finalize()
8传递性A hb B 且 B hb C ⇒ A hb C

最常用的两条

  • volatile 写-读:建立 happens-before
  • synchronized 解锁-加锁:建立 happens-before

4. volatile 的真实作用(2025-2026 面试最爱问)

很多人以为 volatile 保证原子性,其实错的

volatile 真正提供的保证(针对 long/double 以外的变量):

  1. 可见性:写完立刻刷新到主存,读时强制从主存取(或等价效果)
  2. 禁止重排序:volatile 写前面的操作不能排到写后面;读后面的操作不能排到读前面
  • 写 volatile 变量 ≈ StoreStore + StoreLoad 屏障
  • 读 volatile 变量 ≈ LoadLoad + LoadStore 屏障

但它不保证复合操作原子性(如 i++)。

5. final 的内存语义(安全发布)

final 字段在构造器中初始化完成后,后续读这个 final 字段一定能看到正确值(前提是没有 this 逸出)。

这是 JMM 给 immutable 对象最强有力的保证。

6. 快速对比表(面试/理解用)

特性synchronizedvolatilefinal(构造完)AtomicXXX/CAS
原子性是(块)否(只单次读写)是(部分操作)
可见性是(字段级别)
有序性部分(防重排)是(防 this 逸出)依赖实现
使用场景互斥同步状态标志、发布对象不可变对象高并发计数器等

7. 小结(一句话版)

JMM 不是真的内存划分,而是 Java 给多线程程序员的一份“契约”:

  • 你只要遵守 happens-before 规则
  • 我(JVM/编译器/CPU)就保证你的修改对其他线程可见,且不会做破坏顺序的重排序

违背这个契约 → 数据竞争(data race) → 未定义行为(可能崩溃、错乱、死循环、看起来“正常”但其实错)

重阳,你现在对 JMM 最想深入哪个点?

  • 双重检查锁单例为什么必须加 volatile?
  • happens-before 的传递性举例
  • volatile 在 x86/arm 上的真实实现差异
  • JMM vs C++ memory model 对比
  • 实际代码案例分析

随时说,我们继续深挖~

文章已创建 4357

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部