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规则

  • 内存屏障机制

解决了以下三个问题:

  • 可见性

  • 原子性

  • 有序性

相关推荐
一个博客2 小时前
pdf-viewer 实现预览pdf文件
开发语言·javascript·pdf
云烟成雨TD9 小时前
Spring AI Alibaba 1.x 系列【69】Token 用量统计
java·人工智能·spring
JAVA9659 小时前
JAVA面试-并发篇 03-使用synchronized doublecheck实现单例有什么坑
java·单例模式·面试
在繁华处9 小时前
Java从零到熟练(四):面向对象基础
java·开发语言
Unbelievabletobe9 小时前
解决了股票api接口盘后数据更新慢的问题
大数据·开发语言·python
不会C语言的男孩10 小时前
C++ Primer 第2章:变量和基本类型
开发语言·c++
小江的记录本10 小时前
【JVM虚拟机】堆内存分代模型:年轻代(Eden+Survivor)、老年代、元空间Metaspace(附《思维导图》+《面试高频考点清单》)
java·前端·jvm·后端·python·spring·面试
在繁华处10 小时前
Java从零到熟练(三):流程控制
java·开发语言·python
唐青枫11 小时前
Java Optional 实战指南:优雅处理空值与链式转换
java