JVM⑥Java内存模型(JMM)
Java 内存模型
JMM:Java Memory Model
与内存结构(运行时数据区等)是不同的两个概念
作用:让 Java 程序在所有平台具有一致的内存访问效果(而不受硬件和操作系统影响)
- 让 Java 的并发内存访问操作不会产生歧义。
- JVM 能自由利用硬件的特性,获取更好的执行速度。
主内存与工作内存
JMM 定义了程序中各种变量的访问规则
这里的变量不包括局部变量和方法参数。
主内存:所有变量都存储在主内存。
工作内存:每个线程有自己的工作内存,其中保存了该线程所用变量的主内存副本。
-
线程对变量的操作(读取、赋值)只在工作内存中进行,不能直接读写主内存中的数据。
-
线程之间也无法直接访问对方工作内存的变量,需要通过主内存来传递。

说明:主内存与工作内存的概念,与运行时数据区中的堆栈等结构,是不同层次的概念。
两个概念之间基本没有任何关系,如果一定要勉强对应
- 主内存:对应堆中的对象实例数据部分
- 工作内存:对应虚拟机栈的部分区域
三大特性
JMM 围绕并发过程中这三个特性来建立:原子性(Atomicity)、可见性(Visibility)、有序性(Ordering)
原子性
举个例子
两个线程,对一个初值为 0 的静态变量,分别作自增和自减,结果是 0 吗?
分析字节码
-
getstatic:获取静态变量 i 的值
-
iconst_1:从常量池加载一个常量 1
-
iadd / isub:加法/减法
-
putstatic:将修改后的值,赋值给静态变量 i
// i++ 对应字节码 getstatic i iconst_1 iadd putstatic i // i-- 对应字节码 getstatic i iconst_1 isub putstatic i
结果可能是正数、负数、0。
在多线程情况下,对静态变量 i 的操作不是原子性。
假如 8 行字节码交错执行,就可能出现结果非 0 的情况。
解决方法
同步关键字:synchronized
synchronized(Object) {
// 要求原子性操作的代码
}
可见性
举个例子:退不出的循环
线程 t 由于 run == true 进入循环。
现象:主线程将 run 设为 false,但线程 t 没有退出循环。
static boolean run = true;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
while(run){
// ....
}
});
t.start();
Thread.sleep(1000);
run = false;
}
浅分析一下
- 线程 t 开始时,从主内存中将 run 的值读到工作内存。
- 由于线程对变量的操作只在工作内存中进行,不能直接读写主内存中的数据。
- 主内存中的数据改变,而线程 t 只是读取本地内存中的 run,没有从主内存中再次读取 run 值
解决方法
易变关键字:volatile
- 修饰成员变量和静态成员变量
- 要求线程从主内存中读取变量值,而不是从工作缓存。
注意:synchronized 也可以保证原子性,但效率低。
有序性
举个例子:诡异的结果
两个线程分别执行以下两个方法,问 result 的最终结果?
int result = 0;
int num = 1;
boolean ready = false;
// 线程t1执行
public void m1() {
if(ready) {
// ①
result = num + num;
} else {
// ②
result = 1;
}
}
// 线程t2执行
public void m2() {
// ③
num = 2;
// ④
ready = true;
}
一共有以下三种情况:结果可能是 1 或 4。
-
先进入 m1():条件假,进入 else 执行 ②

-
先进入 m2():执行③,④还未执行;此时进入 m1():条件假,执行②

-
先进入 m2():执行③④;再进入 m1():条件真,执行①

还有一种情况:result == 2
指令重排:运行期优化的策略,JVM 会将容易执行的代码优先执行。
-
先进入 m2():执行④,③未执行
-
此时进入 m1():条件真,执行①

解决方法
volatile:可以禁用指令重排。
经典应用:——双重检查加锁(double-checked locking)

分析:如果不加 volatile 关键字
分析字节码:instance = new DoubleChecked();
-
new:创建对象实例。分配堆内存,将对象引用压入操作数栈
-
dup:复制操作数栈的栈顶数据,用于调用初始化(不理解的可以复习)
-
invokespecial:初始化
-
putstatic:将对象引用赋值给变量 instance。
0: new #2 3: dup 4: invokespecial #3 7: putstatic #4
如果出现指令重排,将 4 和 7 的顺序调换。
导致赋值给 instance 的对象实例尚未初始化(null)。
假设线程 t1 执行方法时发生指令重排,线程 t2 再调用该方法时,判断 instance != null,方法返回一个未完成初始化的 instance 。
到这里,就可以体会到 volatile 关键字的作用了。
----
对 JVM 的入门学习暂时告一段落。
复习的时候会对 JVM 进行更系统性的学习,对所有笔记进行补充和改进。