Java内存模型(JMM)完全指南:从硬件架构到并发编程的桥梁
Java内存模型(JMM)完全指南:从硬件架构到并发编程的桥梁
|
🌺The Begin🌺点点关注,收藏不迷路🌺
|
📌 本文导读:本文将系统讲解Java内存模型(JMM)的核心概念,从硬件缓存架构到JMM的抽象定义,从三大特性到happens-before规则,用流程图和代码示例帮助你彻底理解Java并发编程的内存语义。全文包含四大核心章节、3个彩色流程图、5个代码示例。预计阅读时间25分钟。
一、🔴 JMM概述:连接硬件与Java并发的抽象层
1.1 🟠 为什么需要Java内存模型?
在多核时代,CPU的性能提升主要依赖于缓存和指令重排序等优化技术。不同的硬件平台和操作系统在内存访问模型上存在巨大差异。
Java的使命是**“Write Once, Run Anywhere”**——如果Java直接暴露底层硬件的内存访问细节,程序在不同平台上的行为将大相径庭。**Java内存模型(Java Memory Model,简称JMM)**就是为了解决这个问题而诞生的。
📌 一句话理解:JMM是Java虚拟机定义的一套规范,它屏蔽了不同硬件和操作系统的内存访问差异,定义了多线程程序中共享变量的读写规则,确保Java程序在各种平台上都能达到一致的并发效果。
1.2 🟡 JMM的核心定义
根据Java语言规范(JLS),JMM描述了多线程Java程序的可能行为。如果程序的正确性基于单线程的执行特性,在多线程环境下可能是不安全的。两种主要的并发风险是:可见性问题与重排序问题。
1.3 🟢 JMM与JVM内存结构的区别
这是一个容易混淆的概念:
| 维度 | JVM内存结构 | JMM(Java内存模型) |
|---|---|---|
| 关注点 | 运行时数据存储区域 | 多线程并发访问规则 |
| 内容 | 堆、栈、方法区等 | 主内存、工作内存、happens-before |
| 目的 | 描述Java程序运行时的内存划分 | 定义线程间如何交互共享变量 |
二、🔵 流程图:JMM的核心架构——主内存与工作内存
JMM定义了两种内存区域:
| 内存区域 | 说明 | 归属 |
|---|---|---|
| 主内存(Main Memory) | 存储Java程序中的所有共享变量(实例字段、静态字段、数组元素) | 所有线程共享 |
| 工作内存(Working Memory) | 存储线程对共享变量的副本,线程对变量的所有操作都在工作内存中进行 | 线程私有 |
关键规则:线程不能直接操作主内存,必须通过"工作内存与主内存之间的同步"来完成变量的传递。这种设计对应了硬件层面CPU缓存与主内存的关系。
三、🟣 线程间通信的八种原子操作
JMM定义了8种原子操作来完成工作内存与主内存之间的交互:
| 操作 | 作用域 | 说明 |
|---|---|---|
| lock | 主内存 | 将变量标识为线程独占状态 |
| unlock | 主内存 | 释放被锁定的变量 |
| read | 主内存 | 将变量值从主内存传输到工作内存 |
| load | 工作内存 | 将read操作得到的值放入工作内存的变量副本 |
| use | 工作内存 | 将工作内存变量值传递给执行引擎 |
| assign | 工作内存 | 将执行引擎的值赋给工作内存变量 |
| store | 工作内存 | 将工作内存变量值传输到主内存 |
| write | 主内存 | 将store操作得到的值写入主内存变量 |
⚠️ 注意:JMM只要求这些操作是顺序执行的,不要求是连续执行的——read和load之间可以插入其他指令,store和write之间同理。
四、🟤 JMM的三大特性
4.1 🔴 原子性(Atomicity)
定义:一个操作是不可中断的,在多线程环境下,要么全部执行成功,要么全部执行失败。
JMM保证了read、load、assign、use、store、write六个操作的原子性。对于基本数据类型(long和double除外)的读写是原子的。
但更大范围的原子性需要通过锁来保证:
// ❌ 非原子操作
private int count = 0;
public void increment() {
count++; // 相当于:读取→修改→写入,三步可分
}
// ✅ synchronized保证原子性
private int count = 0;
public synchronized void increment() {
count++; // 整个操作在锁保护下原子执行
}
// ✅ 原子类也提供原子性
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
Java提供了monitorenter和monitorexit两个字节码指令来支持更大范围的原子操作,对应到代码中就是synchronized关键字。
4.2 🟠 可见性(Visibility)
定义:当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。
// ❌ 不可见性问题
private boolean running = true;
// 线程1:while (running) { ... } // 可能永远看不到running的变化
// 线程2:running = false; // 修改可能不会立即刷新到主内存
// ✅ volatile保证可见性
private volatile boolean running = true; // 修改立即同步到主内存
JMM中,普通变量与主内存的同步时机是不确定的,可能导致其他线程读取到过期值。保证可见性的四种方式:
| 方式 | 说明 |
|---|---|
| volatile | 每次修改立即写回主内存,每次读取从主内存刷新 |
| synchronized | 获取锁时刷新工作内存,释放锁时写回主内存 |
| Lock | 与synchronized类似 |
| final | 初始化完成后,其他线程可见 |
4.3 🟡 有序性(Ordering)
定义:在本线程观察,所有操作都是有序的;在一个线程观察另一个线程,所有操作都是无序的(由于指令重排序)。
指令重排序是编译器和处理器为了优化性能而对指令执行顺序进行的调整。这可能导致多线程环境下出现看似"不可能发生"的现象。
// 🔴 重排序导致的问题示例
// 假设 a 和 b 是共享变量
int a = 0, b = 0;
// 线程1执行
a = 1; // 语句1
int r1 = b; // 语句2
// 线程2执行
b = 2; // 语句3
int r2 = a; // 语句4
由于语句1和语句2之间没有数据依赖关系,编译器和处理器可能将它们重排序,导致r1和r2读到意想不到的值。
JMM通过happens-before规则来保证有序性,而不是完全禁止重排序。
五、🟢 流程图:happens-before规则全览
happens-before规则的核心价值:如果两个操作之间存在happens-before关系,那么前一个操作的结果对后一个操作是可见的,并且前一个操作的执行顺序优先于后一个操作。
六、🔴 volatile的内存语义与内存屏障
6.1 🟠 volatile的两大承诺
根据JMM规范,volatile关键字提供两个核心承诺:
| 承诺 | 说明 |
|---|---|
| 可见性 | volatile变量的写操作happens-before后续对该变量的读操作 |
| 有序性 | volatile读写操作不会被重排序 |
6.2 🟡 内存屏障插入策略
为了实现volatile的内存语义,JMM采用保守策略在指令序列中插入内存屏障:
volatile写操作的内存屏障:
[普通读写] → StoreStore屏障 → [volatile写] → StoreLoad屏障 → [后续操作]
volatile读操作的内存屏障:
[volatile读] → LoadLoad屏障 → [后续读]
[volatile读] → LoadStore屏障 → [后续写]
四种内存屏障的作用:
| 屏障类型 | 作用 |
|---|---|
| LoadLoad | 确保load1的数据装载优先于load2及后续装载 |
| StoreStore | 确保store1的数据对其他处理器可见优先于store2 |
| LoadStore | 确保load1数据装载优先于store2及后续存储指令刷新到内存 |
| StoreLoad | 全能屏障,确保store1数据对其他处理器可见优先于load2及后续装载 |
七、🔵 避坑指南
| 序号 | 常见误区 | 正确理解 |
|---|---|---|
| ① | JMM就是JVM内存结构 | JMM是并发访问规则,JVM内存结构是运行时数据存储区域 |
| ② | volatile保证原子性 | volatile只保证可见性和有序性,不保证原子性 |
| ③ | 指令重排序一定会出问题 | 重排序遵循"单线程语义不变"原则,只在多线程共享数据时可能出问题 |

|
🌺The End🌺点点关注,收藏不迷路🌺
|
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)