菜单

Administrator
发布于 2026-05-15 / 2 阅读
0
0

图解Java内存模型

图解Java内存模型(JMM)

一、核心定义

Java 内存模型(Java Memory Model,JMM) 是 Java 虚拟机规范定义的并发编程规范。它抽象了线程和主内存之间的关系,规定了从 Java 源代码到 CPU 可执行指令的转化过程中需要遵守的并发原则和规范。

主要目的:简化多线程编程,增强程序的可移植性。


二、为什么需要 JMM?

1. CPU 缓存模型

现代 CPU 的处理速度远快于内存访问速度,因此引入了多级缓存(L1/L2/L3 Cache)来弥合速度差异:

CPU → L1 Cache → L2 Cache → L3 Cache → 主内存

工作方式:将主存数据复制到 Cache 中,CPU 直接读写 Cache,运算后写回主存。

问题:多核 CPU 下,每个核心有自己的缓存,导致缓存不一致性问题。例如两个线程同时执行 i++,可能都从缓存读取 i=1,运算后写回 i=2,但正确结果应为 i=3。

解决方案:缓存一致性协议(如 MESI 协议),在 CPU 层面保证数据一致性。

2. 指令重排序

为了提升性能,编译器和处理器会对指令进行重排序:

重排序类型 说明
编译器优化重排 不改变单线程语义,重新安排语句顺序
指令并行重排 处理器将多条指令重叠执行(ILP)
内存系统重排 缓存/写缓冲区导致加载存储乱序

完整流程

Java 源码 → 编译器优化重排 → 指令并行重排 → 内存系统重排 → 可执行指令

关键问题:指令重排序保证串行语义一致,但不保证多线程语义一致。多线程下可能产生意外结果。

解决方案:插入内存屏障(Memory Barrier) 阻止重排序,强制刷新写缓冲区,使缓存失效。


三、JMM 的核心概念

1. 主内存与工作内存

┌─────────────────────────────────────────────────┐
│                    主内存                         │
│         (所有共享变量、实例字段、静态字段)          │
└─────────────────────────────────────────────────┘
          ↑              ↑              ↑
    ┌──────────┐  ┌──────────┐  ┌──────────┐
    │ 本地内存  │  │ 本地内存  │  │ 本地内存  │
    │ (线程A)   │  │ (线程B)   │  │ (线程C)   │
    └──────────┘  └──────────┘  └──────────┘
  • 主内存:存储所有共享变量(实例字段、静态字段、数组元素)。
  • 本地内存(抽象概念):每个线程私有的工作内存,包含 CPU 缓存、写缓冲区、寄存器等,存储共享变量的副本。
  • 通信规则:线程间无法直接访问对方的本地内存,必须通过主内存进行通信。

2. JMM 与 Java 运行时内存区域的区别

对比项 JMM 运行时内存区域
性质 抽象规则,描述变量访问控制 具体划分,JVM 运行时的内存结构
关注点 原子性、有序性、可见性 堆、栈、方法区、程序计数器
共享区域 主存(对应堆+方法区) 堆、方法区
私有区域 本地内存 程序计数器、虚拟机栈、本地方法栈

四、happens-before 规则

happens-before 是 JMM 最核心的规则,用于描述两个操作之间的内存可见性。

核心思想

如果操作 A happens-before 操作 B,那么操作 A 的执行结果对操作 B 可见,且操作 A 的执行顺序排在操作 B 之前。

设计哲学

  • 对程序员:提供简单易懂的强内存模型保证。
  • 对编译器/处理器:允许优化重排序,只要不改变程序执行结果。

天然 happens-before 关系(JSR-133)

1. 程序顺序规则
   一个线程中的每个操作,happens-before 于该线程中的任意后续操作。

2. 监视器锁规则
   对一个锁的解锁,happens-before 于随后对这个锁的加锁。

3. volatile 变量规则
   对一个 volatile 域的写,happens-before 于任意后续对这个 volatile 域的读。

4. 传递性
   若 A happens-before B,且 B happens-before C,则 A happens-before C。

5. 线程启动规则
   Thread.start() 调用 happens-before 于启动线程的任何动作。

6. 线程终止规则
   线程中所有操作 happens-before 于其他线程检测到该线程终结。

7. 线程中断规则
   interrupt() 调用 happens-before 于被中断线程检测到中断事件。

8. 对象终结规则
   构造函数的结束 happens-before 于 finalizer 的开始。

9. final 字段规则
   final 字段在构造函数中的写,happens-before 于后续任何线程对该字段的读。

示例说明

int a = 1;       // 操作 A
int b = 2;       // 操作 B
int sum = a + b; // 操作 C
  • A happens-before B, B happens-before C, 所以 A happens-before C。
  • 虽然 A 与 B 在代码中是顺序关系,但 JVM 可以重排 A 和 B 的执行顺序,因为不影响最终结果。

五、并发编程三大特性

1. 原子性(Atomicity)

  • 定义:一个或多个操作要么全部执行且不中断,要么都不执行。
  • 实现方式synchronizedLock、原子类(AtomicInteger 等基于 CAS)。
  • 注意volatile 不保证原子性。

2. 可见性(Visibility)

  • 定义:一个线程修改共享变量后,其他线程能立即看到修改后的值。
  • 实现方式synchronizedvolatileLock
  • volatile 原理:每次读取变量都从主内存读取,写入时立即刷新到主内存,并让其他线程的缓存失效。

3. 有序性(Ordering)

  • 定义:程序执行的顺序按代码的先后顺序执行。
  • 问题:指令重排序可能导致多线程下的乱序问题。
  • 实现方式volatile 禁止指令重排序优化,synchronized 保证块内代码的原子执行。

六、内存屏障

内存屏障(Memory Barrier)是 JMM 实现可见性和有序性的底层机制:

屏障类型 作用
LoadLoad 确保 Load1 的读取在 Load2 之前完成
StoreStore 确保 Store1 的写入对 Store2 可见
LoadStore 确保 Load1 的读取在 Store2 写入之前完成
StoreLoad 确保 Store1 写入对后续 Load 可见(最严格)

volatile 关键字在读写时会插入相应的内存屏障来保证可见性和禁止重排序。


七、总结

  1. JMM 是什么:一组并发编程规范,抽象线程与主内存关系,通过 happens-before 规则保证内存可见性。
  2. 为什么需要 JMM:屏蔽不同硬件/操作系统的差异,解决 CPU 缓存不一致和指令重排序问题。
  3. 核心机制
    • happens-before 规则:开发者遵循即可写出正确的并发程序。
    • 内存屏障:底层实现,禁止特定类型的重排序。
  4. 同步手段
    • volatile:保证可见性 + 禁止重排序,不保证原子性。
    • synchronized:保证可见性 + 原子性 + 有序性。
    • final:提供特殊的初始化安全性保证。
  5. 重要区分:JMM ≠ Java 运行时内存区域。JMM 是并发规则,运行时内存区域是 JVM 内存结构。

评论