【JVM】内存模型:原子性、可见性、有序性的问题引出与解决
创始人
2024-02-12 15:35:47

一、内存模型

很多人将【java 内存结构】与【java 内存模型】傻傻分不清,【java 内存模型】是 Java MemoryModel(JMM)的意思。

  • 简单的说,JMM 定义了一套在多线程读写共享数据时(成员变量、数组)时,对数据的可见性、有序性、和原子性的规则和保障

关于它的权威解释,请参考:链接

二、原子性

2.1 指令交错

原子性在学习线程时讲过,下面来个例子简单回顾一下:

问题提出,两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?

public class Atomicity1 {static int a=0;public static void main(String[] args) throws InterruptedException {for (int j=0;j<5;j++) {Thread thread1 = new Thread(() -> {for (int i = 0; i < 5000; i++) {a++;}});Thread thread2 = new Thread(() -> {for (int i = 0; i < 5000; i++) {a--;}});thread1.start();thread2.start();thread1.join();thread2.join();System.out.println("第"+j+"次:"+a);a=0;}}
}
第0次:-2640
第1次:0
第2次:1891
第3次:-1317
第4次:294

2.2 问题分析

以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作

a++ 实际产生的字节码指令:

getstatic a 	// 获取静态变量a的值
iconst_1 		// 准备常量1
iadd 			// 加法
putstatic a 	// 将修改后的值存入静态变量a

i++ 实际产生的字节码指令:

getstatic a 	// 获取静态变量a的值
iconst_1 		// 准备常量1
isub 			// 减法
putstatic a 	// 将修改后的值存入静态变量a

内存模型如下:一个线程要完成静态变量的自增、自减,需要从主内存中获取静态变量的值到线程内存中进行计算,然后再写到主存中

在这里插入图片描述

单线程情况:没有问题

如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题:

getstatic a 	// 线程1-获取主内存静态变量i的值:线程内i=0
iconst_1 		// 线程1-准备常量1
iadd 			// 线程1-加法:线程内i=1
putstatic a 	// 线程1-将修改后的值存入静态变量i:主内存静态变量i=1
getstatic a 	// 线程1-获取静态变量i的值:线程内i=1
iconst_1 		// 线程1-准备常量1
isub 			// 线程1-减法:线程内i=0
putstatic a 	// 线程1-将修改后的值存入静态变量i:主内存静态变量i=0

多线程情况:出现问题

多线程下这 8 行代码可能交错运行(为什么会交错?思考一下):

出现负数的情况之一:

getstatic a 	// 线程1-获取主内存静态变量a的值:线程内a=0
getstatic a 	// 线程2-获取主内存静态变量a的值:线程内a=0
iconst_1 		// 线程1-准备常量1
iadd 			// 线程1-加法:线程内a=1
putstatic a 	// 线程1-将修改后的值存入静态变量a:主内存静态变量a=1
iconst_1 		// 线程2-准备常量1
isub 			// 线程2-减法:线程内a=-1
putstatic a 	// 线程2-将修改后的值存入静态变量a:主内存静态变量a=-1

出现正数的情况之一:

getstatic a 	// 线程1-获取主内存静态变量a的值:线程内a=0
getstatic a 	// 线程2-获取主内存静态变量a的值:线程内a=0
iconst_1 		// 线程1-准备常量1
iadd 			// 线程1-加法:线程内a=1
iconst_1 		// 线程2-准备常量1
isub 			// 线程2-减法:线程内a=-1
putstatic a		// 线程2-将修改后的值存入静态变量a:静态变量a=-1
putstatic a 	// 线程1-将修改后的值存入静态变量a:静态变量a=1

2.3 问题解决

synchronized

  • 优点:可以保证代码块内的 原子性、可见性
  • 缺点:属于重量级操作,性能相对更低

让操作共享变量的线程只能同时存在一个,不让操作a的指令交错执行。使用 synchronized **锁住同一个对象 **进行保证

public class Atomicity1 {static int a=0;static Object lock=new Object();	//锁对象public static void main(String[] args) throws InterruptedException {for (int j=0;j<5;j++) {Thread thread1 = new Thread(() -> {synchronized (lock){	//线程操作共享变量时竞争锁for (int i = 0; i < 5000; i++) {a++;}}});Thread thread2 = new Thread(() -> {synchronized (lock){	//线程操作共享变量时竞争锁for (int i = 0; i < 5000; i++) {a--;}}});thread1.start();thread2.start();thread1.join();thread2.join();System.out.println("第"+j+"次:"+a);a=0;}}
}

三、可见性

3.1 退不出的循环

public class visibility1 {static boolean run=true;public static void main(String[] args) throws InterruptedException {new Thread(()->{while(run){}},"t").start();Thread.sleep(1000);System.out.println("1秒后");run=false;}
}

3.2 问题分析

  1. 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存

    在这里插入图片描述

  2. 因为 t 线程要频繁从主内存中读取 run 的值,JIT 编译器会将 run 的值缓存至自己工作内存中的高速缓存中,减少对主存中 run 的访问,提高效率

    在这里插入图片描述

  3. 1 秒之后,main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值

    在这里插入图片描述

3.3 问题解决

  • volatile(易变关键字)
    • 它可以用来修饰 成员变量和静态成员变量
    • 它可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存
    • 不能保证原子性。仅用在一个写线程,多个读线程的情况
  • synchronized
    • 优点:可以保证代码块内的 原子性、可见性
    • 缺点:属于重量级操作,性能相对更低

给该例子的 run 加上volatile,保障的实际就是可见性问题。从字节码理解是这样的:

getstatic run 	// 线程t:获取 run true
getstatic run 	// 线程t:获取 run true
getstatic run 	// 线程t:获取 run true
getstatic run 	// 线程t:获取 run true
putstatic run 	// 线程main:修改 run 为 false, 仅此一次
getstatic run 	// 线程t:获取 run false

比较一下之前我们将线程安全时举的例子:两个线程一个 i++ 一个 i-- ,此时 volatile 只能保证看到最新值,不能解决指令交错

getstatic a 	// 线程1-获取主存静态变量a的值:线程内a=0
getstatic a 	// 线程2-获取主存静态变量a的值:线程内a=0
iconst_1 		// 线程1-准备常量1
iadd 			// 线程1-加法:线程内a=1
putstatic a 	// 线程1-将修改后的值存入静态变量:主存静态变量a=1
//注意:此时线程2无需再执行取值操作,所以线程1存值时就算有 volatile 也于事无补
iconst_1 		// 线程2-准备常量1
isub 			// 线程2-减法:线程内a=-1
putstatic a 	// 线程2-将修改后的值存入静态变量:主存静态变量a=-1

思考

如果在前面示例的死循环中加入 System.out.println() 会发现即使不加 volatile 修饰符,线程 t 也能正确看到对 run 变量的修改了,想一想为什么?

四、有序性

4.1 出现指令重排

有一种现象叫做 指令重排 ,是 JIT 编译器在运行时的一些优化,这个现象需要通过大量测试才能复现

int num = 0;
boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {if(ready) {r.r1 = num + num;} else {r.r1 = 1;}
}
// 线程2 执行此方法
public void actor2(I_Result r) {num = 2;ready = true;
}

I_Result 是一个对象,有一个属性 r1 用来保存结果,问,可能的结果有几种?有同学分析了三种情况

  • r1 = 1:线程1 直接执行完。
  • r1 = 4:线程2 直接执行完。
  • r1 = 1:线程2 先执行到 num = 2,但没来得及执行 ready = true,线程1 执行进入 else 分支

r1=0:JIT 进行指令重排导致的有序性问题

这种情况下是:线程2 直接执行 ready = true,切换到 线程1 进入 if 分支,相加为 0,再切回线程2 执行num = 2。

4.2 分析

JVM 会在不影响正确性的前提下,可以调整语句的执行顺序,思考下面一段代码

static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...; // 较为耗时的操作
j = ...;

可以看到,至于是先执行 i 还是 先执行 j ,对最终的结果不会产生影响。所以,上面代码真正执行时,既可以是

i = ...; // 较为耗时的操作
j = ...;

也可以是

j = ...;
i = ...; // 较为耗时的操作

这种特性称之为**『指令重排』**。多线程下『指令重排』会影响正确性

单例应用:双重检测

/*** 加 volatile 的原因:*		1.线程1:new 关键字给INSTANCE分配空间,此时INSTANCE不为null*		2.线程2:获取到了还没完全初始化好的 INSTANCE*/
public final class Singleton {private Singleton() { }private static volatile Singleton INSTANCE = null;public static Singleton getInstance() {//1.实例未创建才竞争if (INSTANCE == null) {synchronized (Singleton.class) {//2.前面获得锁的线程已经创建对象了if (INSTANCE == null) {INSTANCE = new Singleton();}}}return INSTANCE;}
}

以上的实现特点是:

  • 懒惰实例化
  • 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁

4.3 解决方法

volatile 修饰的变量,可以禁用指令重排

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...