【JVM】内存模型:原子性、可见性、有序性的问题引出与解决

news2024/9/23 3:20:49

一、内存模型

很多人将【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次:-26401次:02次:18913次:-13174次: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 修饰的变量,可以禁用指令重排

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/32727.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

(七)RabbitMQ持久化

RabbitMQ持久化1、概念2、队列持久化3、消息持久化4、不公平分发5、预取值1、概念 默认情况下 RabbitMQ 退出或由于某种原因崩溃时&#xff0c;它忽视队列和消息&#xff0c;除非告知它不要这样做。确保消息不会丢失需要做两件事&#xff1a;我们需要将队列和消息都标记为持久化…

广义OOD检测最新综述

arXiv在2021年10月21日上传的论文**“Generalized Out-of-Distribution Detection: A Survey“&#xff0c;作者来自新加坡的南洋理工大学&#xff08;NTU&#xff09;和美国的威斯康星大学Madison分校**。 OOD检测对确保机器学习系统的可靠性和安全性至关重要&#xff0c;例如…

秦皇岛科学选育新品种 国稻种芯·中国水稻节:河北谱丰收曲

秦皇岛科学选育新品种 国稻种芯中国水稻节&#xff1a;河北谱丰收曲 冀时客户端报道&#xff08;河北台 张志刚 米弘钊 赵永鑫&#xff09; 新闻中国采编网 中国新闻采编网 谋定研究中国智库网 国稻种芯中国水稻节 中国农民丰收节国际贸易促进会 中国三农智库网-功能性农业农业…

Java对象拷贝原理剖析及最佳实践

作者&#xff1a;宁海翔 1 前言 对象拷贝&#xff0c;是我们在开发过程中&#xff0c;绕不开的过程&#xff0c;既存在于Po、Dto、Do、Vo各个表现层数据的转换&#xff0c;也存在于系统交互如序列化、反序列化。 Java对象拷贝分为深拷贝和浅拷贝&#xff0c;目前常用的属性拷…

2023年系统规划与设计管理师-学习计划安排

一.学习计划和安排&#xff1a; 序号 学习内容 分数以及题型 学习安排 完成日期 1 浏览教程&#xff0c; 了解知识结构 1天 11/24 2 前三章内容&#xff1a; 课本&#xff0c; 单元练习&#xff0c; 思维导图&#xff0c; 总结归纳&#xff0c; 第一遍背诵 分数占…

希望所有计算机学生都知道这些宝藏课程

数据结构 青岛大学——王卓老师的数据结构与算法基础 浙江大学——陈越、何钦铭老师的数据结构课程 清华大学——邓俊辉老师的数据结构课程 北京大学——数据结构基础课程 操作系统 哈工大——李治军老师的操作系统 清华大学——操作系统原理 南京大学——操作系统概述 计算…

机器学习知识经验分享之一:卷积神经网络介绍

文章目录前言一、卷积神经网络的构成1.卷积层2.池化层3.激活函数4.批量归一化5.损失函数二、卷积神经网络的特点总结前言 本系列文章将对机器学习知识进行分享总结。便于大家从理论层面了解人工智能基础原理&#xff0c;从而更好的运用算法发论文写作以及实际应用。关注即免费…

CNI设计解读

何为cni&#xff1f; kubernetes在设计网络方案的时候并没有设计统一的网络方案&#xff0c;只提供了统一的容器网络接口也就是所谓cni&#xff0c;这么做的目的就是为了遵循kubernets的核心理念OutOfTree&#xff0c;简单来讲就是专注于自身核心能力&#xff0c;将其他能力类…

使用albumentations对coco进行数据增强

数据增强的必要性 目前几乎所有描述最先进的图像识别模型的论文都使用了基本的增强技术 深度神经网络需要大量的训练数据来获得良好的结果&#xff0c;并防止过度拟合&#xff0c;然而要获得足够的训练样本往往非常困难&#xff0c;多种原因可能使得收集足够的数据非常困难&a…

【计算机毕业设计】27.仓库管理系统源码

一、系统截图&#xff08;需要演示视频可以私聊&#xff09; 摘 要 网络的广泛应用给生活带来了十分的便利。所以把仓库管理与现在网络相结合&#xff0c;利用JSP技术建设仓库管理系统&#xff0c;实现仓库管理系统的信息化。则对于进一步提高公司的发展&#xff0c;丰富仓库管…

户外运动耳机推荐、十大户外运动耳机品牌推荐排名清单

最近南方的天气有点秋高气爽&#xff0c;这样的天气要说最适合进行什么运动&#xff0c;那户外徒步肯定是最佳选择&#xff0c;在这样适宜的天气下去拥抱大自然&#xff0c;体验户外山野环境的美好绝对是个很棒的过程&#xff01;但是一个人的长时间徒步多少还是会少了些味道&a…

408 | 大纲知识点考点冲刺 复习整理 ——【计网】第三章 数据链路层

自用冲刺笔记整理。 部分图片来自王道。 加油ヾ(◍∇◍)ノ゙ (一)数据链路层的功能 结点: 主机、 路由器。帧 : 链路层的协议数据单元, 封装网络层数据报。其主要作用是加强物理层传输原始比特流的功能,将物理层提供的可能出错的物理连接改造成为逻辑上无差错的数据链路,…

11.24Spring学习第四天

整合Mybatis(重点) 步骤 1.引入依赖 <!--引入相关依赖--><!-- spring jdbc --><dependency><groupId>org.springframework</groupId><artifactId>spring-jdbc</artifactId><version>${spring.version}</version></…

在字节跳动做了5年软件测试,12月无情被辞,想给划水的兄弟提个醒...

前言 先简单交代一下背景吧&#xff0c;某不知名 985 的本硕&#xff0c;17 年毕业加入字节&#xff0c;以“人员优化”的名义无情被裁员&#xff0c;之后跳槽到了有赞&#xff0c;一直从事软件测试的工作。之前没有实习经历&#xff0c;算是5年的工作经验吧。 这5年之间完成…

如何在数据库只保存oss上的文件名, 当查询数据时根据字段的文件名, 获取oss的公网访问地址,并对字段内容重写

如何在数据库只保存oss上的文件名, 当查询数据时根据字段的文件名, 获取oss的公网访问地址,并对字段内容重写. 有这样一个需求, 图片上传到oss 上, 返回文件名和公网访问地址, 但是要求数据库中只存储文件名称. 有两个目的: 数据库只存储文件名称, 方便后期oss 上数据迁移到其他…

面试官:在 Java 中 new 一个对象的流程是怎样的?彻底被问懵了。。

对象怎么创建&#xff0c;这个太熟悉了&#xff0c;new一下(其实还有很多途径&#xff0c;比如反射、反序列化、clone等&#xff0c;这里拿最简单的new来讲)&#xff1a; Dog dog new Dog();我们总是习惯于固定语句的执行&#xff0c;却对于背后的实现过程缺乏认知&#xff0…

[附源码]java毕业设计医院门诊信息管理系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

【多线程 (二)】线程安全问题、同步代码块、同步方法、Lock锁、死锁

文章目录线程安全问题前言2.1多线程模拟卖票出现的问题2.2卖票案例中出现的问题分析2.3同步代码块解决数据安全问题2.4同步方法解决数据安全问题2.5Lock锁2.6死锁总结线程安全问题 前言 之前我们讲了多线程的基础知识&#xff0c;但是在我们解决实际问题中会遇到一些错误&…

接口自动化测试实战之智能场景如何攻破

智能场景的意思就是怎么样才能让接口自动化智能化&#xff0c;让使用接口框架的人越来越没有要求&#xff0c;大街上随便拉一个人来&#xff0c;一分钟了解框架的使用&#xff0c;就能完美地去完成接口自动化测试。 1.找出公司要求我们测试的接口的共同点 假设有10个接口&…

【附源码】计算机毕业设计JAVA移动电商网站

【附源码】计算机毕业设计JAVA移动电商网站 目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; JAVA mybati…