JVM进阶系列:彻底理解 Java 内存模型(JMM)

目录

[一、什么是 JMM](#一、什么是 JMM)

[二、为什么需要 JMM](#二、为什么需要 JMM)

[1. CPU缓存导致可见性问题](#1. CPU缓存导致可见性问题)

[2. 指令重排序导致有序性问题](#2. 指令重排序导致有序性问题)

[3. 线程切换导致原子性问题](#3. 线程切换导致原子性问题)

4、小结

5、JMM的本质

[三、JMM 的核心抽象模型](#三、JMM 的核心抽象模型)

[主内存(Main Memory)](#主内存(Main Memory))

[工作内存(Working Memory)](#工作内存(Working Memory))

[四、JMM 引发的三大并发问题](#四、JMM 引发的三大并发问题)

[1. 可见性问题](#1. 可见性问题)

[2. 原子性问题](#2. 原子性问题)

[3. 有序性问题](#3. 有序性问题)

[五、JMM 如何解决这些问题](#五、JMM 如何解决这些问题)

[六、Happens-Before 规则](#六、Happens-Before 规则)

[七、JMM 的实现手段](#七、JMM 的实现手段)

八、总结

一、什么是 JMM

JMM(Java Memory Model,Java 内存模型)是 Java 官方定义的一套并发编程规范

它不是实际存在的内存结构,而是一套规则,用于规范多线程环境下共享变量的访问行为

JMM 的目标是:

  • 屏蔽不同 CPU 架构的差异

  • 屏蔽不同缓存模型的差异

  • 保证 Java 程序在不同平台上的并发行为一致

JMM 就是 Java 为多线程读写共享变量制定的一套规则

二、为什么需要 JMM

现代计算机为了提升运行效率,引入了大量性能优化机制,这些优化让程序运行得更快,但同时也给多线程编程带来了新的问题

主要包括:

  • CPU缓存

  • 指令重排序

  • 线程切换

这些机制分别对应并发编程中的:

  • 可见性问题

  • 有序性问题

  • 原子性问题

1. CPU缓存导致可见性问题

为什么需要CPU缓存

CPU的执行速度远远快于内存,所以为了减少CPU等待内存的时间,现代计算机引入了多级缓存:

CPU寄存器

L1 Cache

L2 Cache

L3 Cache

主内存(RAM)

当CPU读取变量时:

java 复制代码
int count = 0;

通常不会每次都访问主内存,而是先把数据加载到缓存中,这样可以大幅提高性能:

主内存

CPU缓存

CPU执行

CPU缓存带来的问题

假设存在共享变量:

java 复制代码
boolean flag = true;

线程A:

java 复制代码
while (flag) {

}

线程B:

java 复制代码
flag = false;

按照直觉:

线程B修改flag

线程A退出循环

但实际运行时:

线程A可能永远不会退出
原因是:线程A把flag=true缓存到了CPU缓存

之后一直读取缓存:

CPU缓存

flag=true

线程B虽然修改了:

主内存

flag=false

但线程A并没有重新读取主内存,因此就会导致 while(true)无限循环

这就是可见性问题:

一个线程修改了变量,另一个线程无法立即看到最新值

2. 指令重排序导致有序性问题

什么是指令重排序

为了提高CPU执行效率,编译器和CPU会对指令顺序进行优化

例如:

java 复制代码
int a = 1;
int b = 2;

理论执行顺序:

a=1

b=2

但实际上可能执行:

b=2

a=1

因为:两个操作互不依赖,执行结果完全一致

这种优化称为:

Instruction Reordering

指令重排序

指令重排序带来的问题

假设:

java 复制代码
int num = 0;
boolean ready = false;

线程A:

java 复制代码
num = 42;
ready = true;

线程B:

java 复制代码
if (ready) {
    System.out.println(num);
}

正常理解:

num=42

ready=true

因此:

ready=true时

num一定是42

但实际上CPU可能重排序:

java 复制代码
ready = true;
num = 42;

于是出现:

线程B读取ready=true

线程B读取num=0

最终输出:0

这就是有序性问题:

程序执行顺序与代码编写顺序不一致

3. 线程切换导致原子性问题

什么是线程切换

CPU核心数量有限,例如:8核CPU

但程序中可能有:100个线程

CPU无法同时执行所有线程,因此操作系统会不断切换:

线程A

线程B

线程C

线程A
这种过程称为:上下文切换(Context Switch)

线程切换带来的问题

假设:

java 复制代码
int count = 0;

两个线程同时执行:

java 复制代码
count++;

很多人认为:最终结果为2

实际上:count++; 并不是原子操作

底层会拆成:

读取count

count+1

写回count

假设执行过程如下:

线程A:

读取count=0

此时发生线程切换

线程B:

读取count=0

count+1

写回count=1

再次切换回线程A:

count+1

写回count=1

最终结果:

count=1

而不是:count=2

这就是原子性问题:

一个操作在执行过程中被其他线程打断,导致结果不符合预期

4、小结

现代计算机为了提升性能,引入了:

CPU缓存

导致可见性问题

指令重排序

导致有序性问题

多个线程对共享数据进行竞争

导致原子性问题

为了统一规范这些行为,屏蔽不同硬件平台之间的差异,Java提出了JMM(Java Memory Model)

JMM通过定义一套统一的内存访问规则,主要解决可见性和有序性问题,原子性则需要依赖 synchronized、Lock、CAS 等同步机制实现

5、JMM的本质

JMM其实是在做一个平衡:性能 VS 正确性

如果追求绝对正确:

所有变量实时同步

禁止所有重排序

但是会导致性能极差

如果追求绝对性能:

无限缓存

无限优化

无限重排序

程序可能错误

所以JMM的目标:

尽可能允许优化

同时保证程序正确

三、JMM 的核心抽象模型

为了统一描述多线程访问共享变量的行为,JMM 抽象出了两个概念:

主内存(Main Memory)

所有共享变量存放的位置,所有线程共享

可以理解为:RAM

工作内存(Working Memory)

每个线程私有,保存共享变量的副本

可以理解为:CPU Cache寄存器

模型如下:

复制代码
             主内存
        +-------------+
        | 共享变量     |
        +-------------+

          ↑       ↑
          ↓       ↓

线程A工作内存   线程B工作内存

线程不能直接操作主内存,必须:

主内存

工作内存

执行修改

刷新主内存

四、JMM 引发的三大并发问题

由于线程操作的是变量副本,因此会产生三类经典问题

1. 可见性问题

线程A修改变量,线程B无法立即看到

例如:

java 复制代码
boolean flag = true;

线程A:

java 复制代码
while(flag){}

线程B:

java 复制代码
flag = false;

线程A可能永远不会退出循环 ,原因是线程A始终读取缓存中的旧值

2. 原子性问题

例如:

java 复制代码
count++;

看似一个操作,实际上包含:读取 + 计算 + 写回

多个线程同时执行时会产生数据覆盖

3. 有序性问题

例如:

java 复制代码
num = 42;
ready = true;

可能被重排序为:

java 复制代码
ready = true;
num = 42;

导致其他线程读取到错误结果

五、JMM 如何解决这些问题

JMM并没有禁止CPU优化,而是定义了一套可见性和有序性规则

核心思想:

如果两个操作满足 Happens-Before 关系,那么前一个操作的结果必须对后一个操作可见

六、Happens-Before 规则

Happens-Before 可以理解为:

前面的操作结果,对后面的操作一定可见

常见规则:

程序次序规则

同一线程中:

java 复制代码
a = 1;
b = 2;

a=1 Happens-Before b=2

volatile规则

写 volatile 变量:

复制代码
flag = true;

Happens-Before

读取 volatile 变量:

复制代码
if(flag)

锁规则

释放锁:

复制代码
unlock()

Happens-Before

后续获取同一把锁:

复制代码
lock()

Thread.start()规则

启动线程前的操作,对新线程可见

Thread.join()规则

线程结束前的操作,对调用 join() 的线程可见

理解Happends-Before

JMM 确实定义了 Happens-Before 规则,但 JMM 并不会自动给所有操作建立 Happens-Before 关系

所以:

JMM 存在 ≠ 所有线程之间都有 Happens-Before

JMM 想解决的问题是:

CPU缓存

编译器优化

指令重排序

多核CPU

导致的并发行为不一致,因此 JMM 制定了一套规则:

如果满足某些条件

必须保证可见性和有序性

这个条件就是:Happens-Before

JMM 并不是自动同步所有线程

假设:

java 复制代码
int num = 0;

线程A:
num = 10;

线程B:
System.out.println(num);

这里:线程A写num , 线程B读num

JMM 不会帮你同步,因为:

没有任何 Happens-Before 关系

所以:0、10都堆

加速理解

JMM 像交通法规:

红灯停

绿灯行

限速120

法规存在,但你不开车遵守:

闯红灯

超速

法规不会自动帮你刹车

JMM 也是一样:

java 复制代码
volatile
synchronized
Lock
start
join

这些就是需要主动使用的规则

使用后:JMM 才提供保证

七、JMM 的实现手段

JMM 本身只是规范

真正实现这些规则依赖于:

volatile

保证:

  • 可见性

  • 禁止指令重排序

但不保证原子性

synchronized

保证:

  • 原子性

  • 可见性

  • 有序性

Lock

与 synchronized 类似,提供更灵活的锁控制能力

CAS

通过硬件级原子指令实现无锁并发,大量应用于 JUC 包

八、总结

JMM 并不是物理内存结构,而是一套并发访问规范

它通过:

  • 主内存与工作内存模型

  • Happens-Before规则

  • 内存屏障机制

解决了以下三个问题:

  • 可见性

  • 原子性

  • 有序性

相关推荐
朦胧之18 小时前
AI 编程-老项目改造篇
java·前端·后端
程序猿大帅1 天前
别再只当调包侠了:用 Spring AI 落地 Function Calling,我被大模型硬生生砸出了三个大坑
java
程序员晓琪1 天前
约定大于配置:基于 Java 包名自动生成 API 版本路由的最佳实践
java·spring boot·后端
Flittly1 天前
【AgentScope Java新手村系列】(11)中断与恢复
java·spring boot·spring
众少成多积小致巨1 天前
JNI (Java Native Interface) 技术手册中文参考指南
android·java·c++
东坡白菜1 天前
破局全栈:前端开发的Java入门实战记录—JPA(2)
java·后端
SimonKing1 天前
艹,维护AI写的代码,我心态崩了......
java·后端·程序员
用户298698530141 天前
Java Word 文档样式进阶:段落与文本背景色设置完全指南
java·后端
小bo波2 天前
从"任意文件复制"深挖Java I/O:字符流与字节流的本质抉择
java·nio·io流·后端开发·文件复制
nanxun8863 天前
记一次诡异的 Docker 容器"串包"故障排查
java