什么是JMM?
JMM即Java Memory Model
Java 内存模型(JMM) 是一套规范,它规定了多线程环境下:
- 如何安全地操作共享数据(比如变量、对象);
- 线程何时能看到其他线程对数据的修改(可见性);
- 代码的执行顺序如何被正确维护(有序性)。
一句话总结:JMM 是多线程编程的“交通规则”。
它告诉程序在并发场景中:
- 什么操作必须按顺序执行(禁止重排序)
- 什么时刻必须刷新数据(保证可见性)
- 什么情况下操作必须完整执行(原子性)
理解JMM,首先需要明白java的运行时数据区,如下图:
- 方法区:存储了运行时常量池、字段和方法参数、构造方法等字节码数据。
- 堆:几乎所有的对象实例及数组都存在这里。
- 栈:每一个线程有一个私有的栈,用于存储局部变量、栈帧、动态链接、方法出口等。
- 本地方法栈:与栈类似,不过进入本地方法栈的数据需要通过native关键字表明。
- 程序计数器:每个线程都有一个独立的程序计数器,用于表示当前线程执行到了字节码的哪一行。
JMM 的工作原理(图解)
对线程来说,栈中的数据不会在线程之间共享,堆中的数据被所有线程共享。所以线程中的可见性问题就是在堆中产生的。
下图中的主存就是堆(这里只是换了一种叫法)存放了线程们共享的变量,线程自己的工作内存中存放了只有自己能访问的变量。
JMM有一个规定:线程对共享变量的所有操作必须在自己的工作内存中执行,不能直接到主存操作或者自己从主存中读取。
为什么需要 JMM?
例子1:
假设你和同事使用Git提交代码:
- 主内存:远程仓库(公司仓库)的代码(唯一权威版本)
- 工作内存:每个人本地的Git仓库
- 问题:你修改了本地代码并提交到了仓库,但未同步到远程仓库,同事到远程仓库看到的是旧版本,而不是最新的代码。
这就是 JMM 要解决的核心问题:多线程环境下,如何保证共享变量的可见性和执行顺序。
例子2:
假如线程1想要访问线程2的变量值有以下步骤(这个过程就是线程之间的通信):
前提:线程2将自己工作内存的值放到了主存中。
- 线程1到工作内存中查找
- 工作内存到主存中查找
- 如果主存中没有则找不到。
这里有一个问题,线程2什么时候将自己工作内存的变量放到放到主存呢?
这是通过JMM来控制的。
JMM三大特征
特性 | 说明 | 保障手段 |
---|---|---|
原子性 | 操作不可分割(要么全做,要么不做) | synchronized、Lock |
可见性 | 线程修改后其他线程立即可见 | volatile、synchronized |
有序性 | 程序执行顺序符合预期 | volatile、happens-before |
JMM 与运行时内存区的区别
JMM(Java Memory Model) | Java 运行时内存区域 | |
---|---|---|
定位 | 并发编程的规范(是一个抽象概念) | 内存物理划分(是真实的内存区域) |
核心 | 多线程之间变量的访问规则 | 内存空间 |
主要内容 | 原子性、可见性、有序性 | 堆、栈、方法区等内存划分 |
典型问题 | 指令重排导致结果异常 | 内存溢出、垃圾回收 |
指令重排序的真相
为什么会重排序?
在保证结果正确的前提下提升效率
假设你要按顺序完成三件事:
- 烧水(5分钟)
- 洗碗(3分钟)
- 泡茶(需要烧好水)
优化后的顺序:先烧水 → 趁烧水时洗碗 → 水开后泡茶
这就是典型的指令重排序——在保证结果正确的前提下提升效率。
happens-before 规则(核心!)
为什么有happens-before
- 开发者需要 JMM 提供一个强大的内存模型来编写代码
- 编译器和处理器希望 JMM 对它们的束缚越少越好,这样它们就可以尽可能多的做优化来提高性能。
所以,JMM 提供了happens-before 规则(JSR-133 规范),我们只要遵循 happens-before 规则,那么写的程序就能保证在 JMM 中具有强的内存可见性。
1. 八大规则详解
规则 | 说明 | 代码示例 |
---|---|---|
程序顺序规则 | 代码顺序保证结果 | 单线程代码顺序执行 |
锁规则 | 解锁先于后续加锁 | synchronized块 |
volatile规则 | 写操作先于后续读操作 | volatile变量读写 |
传递性规则 | A→B且B→C,则A→C | 组合多个规则 |
线程启动规则 | start()先于线程内任何操作 | thread.start() |
线程终止规则 | 线程最后操作先于终止检测 | thread.join() |
中断规则 | interrupt()先于检测中断 | thread.interrupt() |
对象终结规则 | 构造函数先于finalize() | 对象初始化 |
2. 案例解析
// 正确使用 volatile 的案例
public class VolatileDemo {
private volatile boolean flag = false;
private int value = 0;
public void writer() {
value = 42; // 普通写
flag = true; // volatile写(happens-before后续读)
}
public void reader() {
if (flag) { // volatile读
System.out.println(value); // 一定能看到42
}
}
}
通过 volatile 写操作建立 happens-before 关系,保证普通变量的可见性。
JMM 实现原理
1. 内存屏障类型
屏障类型 | 作用 |
---|---|
LoadLoad屏障 | 禁止读与读重排序 |
StoreStore屏障 | 禁止写与写重排序 |
LoadStore屏障 | 禁止读与写重排序 |
StoreLoad屏障 | 禁止写与读重排序(全能屏障) |
2. volatile 实现原理
// 查看汇编指令(使用hsdis插件)
public class VolatileTest {
private volatile int v;
public void write() {
v = 1; // 会插入StoreStore + StoreLoad屏障
}
public void read() {
int tmp = v; // 会插入LoadLoad + LoadStore屏障
}
}
面试常见问题
1:synchronized 能保证可见性吗?
答:可以!因为:
- 解锁前必须把变量刷回主内存
- 加锁时会清空工作内存,从主内存重新加载
2:final 关键字的内存语义?
- 构造函数内对 final 域的写入,与后续引用赋值不能重排序
- 初次读取包含 final 域的对象引用时,会保证 final 域已初始化
不加 volatile 时,可能返回未初始化完成的对象(对象创建的非原子操作)。