JMM及其相关

splend21 Lv1

Java 内存模型(Java Memory Model,简称 JMM)是 Java 并发编程的基础。它不是真实存在的物理内存布局,而是一套由 Java 虚拟机(JVM)定义的规范协议

JMM 决定了一个线程对共享变量的写入何时对另一个线程可见,旨在屏蔽硬件和操作系统的内存访问差异,实现 Java 程序在各种平台下一致的并发行为。

1. 为什么需要 JMM?

在现代计算机体系中,为了弥补 CPU 运算速度与内存 IO 速度的巨大鸿沟,硬件设计引入了多级缓存(L1, L2, L3)。同时,为了优化执行效率,编译器和处理器会对指令进行重排序。

这些优化在多线程环境下带来了两个严峻挑战:

  1. 可见性问题:线程 A 在 CPU 缓存中修改了变量,但尚未同步到主内存,线程 B 看到的仍是旧值。
  2. 有序性问题:指令重排(Reordering)可能导致代码执行顺序被打乱。虽然单线程下有 as-if-serial 语义保证结果不变,但在多线程下会产生逻辑错误(如 DCL 单例中对象尚未初始化完毕就被引用)。

2. JMM 的抽象结构

JMM 规定所有的共享变量都存储在 主内存 (Main Memory) 中,每个线程都有自己的 工作内存 (Working Memory)

  • 主内存:物理上对应计算机的 RAM。
  • 工作内存:线程私有的空间。物理上对应 CPU 寄存器和缓存。线程必须先将变量拷贝到工作内存才能操作。

3. 并发编程的三大特性

3.1 原子性

指操作不可分割。synchronized 和原子类等可保证原子性。

3.2 可见性

指一线程修改后,其他线程立即可见。主要靠 volatile 触发缓存同步保证。

3.3 有序性

指程序执行的顺序。volatile 通过内存屏障禁止指令重排来保证。

4. volatile 深度原理:内存屏障

volatile 解决可见性有序性的底层机制是完全相同的,都依赖于 内存屏障。这些屏障规定了指令执行的边界。

4.1 如何保证“有序性” (禁止重排)

Volatile 可以防止指令重排序。啥叫重排序?就是编译器和 CPU 为了效率高,可能会把代码执行顺序调整一下。但多线程下,这可能会出问题。

JVM 在 volatile 读写操作时插入特定的屏障,阻断了指令跨越屏障进行重排序的可能性:

  • 写操作前的屏障:禁止上面的普通写和下面的 volatile 写重排序。这保证了在 volatile 变量生效前,之前的所有写入都已完成。
  • 写操作后的屏障:防止上面的 volatile 写与下面可能的 volatile 读/写重排序。这是全能型屏障,确保写操作彻底同步。
  • 读操作后的屏障:防止上面的 volatile 读与下面后续的读/写操作重排序。这保证了读取到最新值后,后续逻辑才开始执行。

4.2 如何保证“可见性” (数据同步)

  • 写 Volatile 变量时:JVM 在写完后加个写屏障,把变量的新值刷到主内存,确保其他线程能看到。比如线程 A 写 volatile int x = 10;,写屏障大声喊:“我写完了!x 现在是 10!”保证其他线程立马知道。
  • 读 Volatile 变量时:JVM 在读之前加个读屏障,强制问一句:“你们写完了吗?我要读最新的!”然后从主内存拿最新值。比如线程 B 读 x,读屏障确保拿到的是 10,而不是缓存里的 0。

4.3 典型案例:DCL 单例模式

如果不加 volatile,instance = new Singleton() 可能被错误地拆解为:1.分配空间 -> 2.赋值给 instance -> 3.初始化对象(正确为132)。 也就是另一个线程可能拿到一个尚未初始化完成的非空对象。volatile 的内存屏障严禁 2 和 3 重排,从而保证了安全性。

5. Happens-Before 原则

JMM 提供的更高层抽象规则,只要符合以下规则,JMM 保证操作 A 的结果对 B 可见:

  1. 程序次序规则:单线程内代码顺序执行。
  2. 锁定规则:unlock 必然发生在后续对同锁的 lock 之前。
  3. volatile 变量规则:对 volatile 的写操作先行发生于后面对它的读操作。
  4. 传递性:A -> B, B -> C 则 A -> C。
Comments