看完了你还能不懂JAVA内存模型(JMM),我输了

news2024/12/23 19:10:02

前言

开篇一个例子,我看看都有谁会?如果不会的,或者不知道原理的,还是老老实实看完这篇文章吧。

@Slf4j(topic = "c.VolatileTest") public class VolatileTest { static boolean run = true; public static void main(String[] args) throws InterruptedException { Thread t = new Thread(() -> { while (run) { 
// do other things } 
// ?????? 这行会打印吗? log.info("done ....."); }); t.start(); Thread.sleep(1000); 
// 设置run = false run = false; } } 复制代码

main函数中新开个线程根据标位run循环,主线程中sleep一秒,然后设置run=false,大家认为会打印"done ......."吗?

答案就是不会打印,为什么呢?

JAVA并发三大特性

我们先来解释下上面问题的原因,如下图所示,

​现代的CPU架构基本有多级缓存机制,t线程会将run加载到高速缓存中,然后主线程修改了主内存的值为false,导致缓存不一致,但是t线程依然是从工作内存中的高速缓存读取run的值,最终无法跳出循环。

可见性

正如上面的例子,由于不做任何处理,一个线程能否立刻看到另外一个线程修改的共享变量值,我们称为"可见性"。

如果在并发程序中,不做任何处理,那么就会带来可见性问题,具体如何处理,见后文。

有序性

有序性是指程序按照代码的先后顺序执行。但是编译器或者处理器出于性能原因,改变程序语句的先后顺序,比如代码顺序"a=1; b=2;",但是指令重排序后,有可能会变成"b=2;a=1", 那么这样在并发情况下,会有问题吗?

在单线程情况下,指令重排序不会有任何影响。但是在并发情况下,可能会导致一些意想不到的bug。比如下面的例子:

public class Singleton { static Singleton instance; static Singleton getInstance(){ if (instance == null) { synchronized(Singleton.class) { if (instance == null) instance = new Singleton(); } } return instance; } } 复制代码

假设有两个线程 A、B 同时调用 getInstance() 方法,正常情况下,他们都可以拿到instance实例。

但往往bug就在一些极端的异常情况,比如new Singleton() 这个操作,实际会有下面3个步骤:

  1. 分配一块内存 M;

  2. 在内存 M 上初始化 Singleton 对象;

  3. 然后 M 的地址赋值给 instance 变量。

现在发生指令重排序,顺序变为下面的方式:

  1. 分配一块内存 M;

  2. 将 M 的地址赋值给 instance 变量;

  3. 最后在内存 M 上初始化 Singleton 对象。

优化后会导致什么问题呢?我们假设线程 A 先执行 getInstance() 方法,当执行完指令 2 时恰好发生了线程切换,切换到了线程 B 上;如果此时线程 B 也执行 getInstance() 方法,那么线程 B 在执行第一个判断时会发现 instance != null ,所以直接返回 instance,而此时的 instance 是没有初始化过的,如果我们这个时候访问 instance 的成员变量就可能触发空指针异常。

这就是并发情况下,有序性带来的一个问题,这种情况又该如何处理呢?

当然,指令重排序并不会瞎排序,处理器在进行重排序时,必须要考虑指令之间的数据依赖性。

原子性

​如上图所示,在多线程的情况下,CPU资源会在不同的线程间切换。那么这样也会导致意向不到的问题。

比如你认为的一行代码:count += 1,实际上涉及了多条CPU指令:

  • 指令 1:首先,需要把变量 count 从内存加载到 CPU 的寄存器;

  • 指令 2:之后,在寄存器中执行 +1 操作;

  • 指令 3:最后,将结果写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)。

操作系统做任务切换,可以发生在任何一条CPU 指令执行完。假设 count=0,如果线程 A 在指令 1 执行完后做线程切换,线程 A 和线程 B 按照下图的序列执行,那么我们会发现两个线程都执行了 count+=1 的操作,但是得到的结果不是我们期望的 2,而是 1。

​我们潜意识认为的这个count+=1操作是一个不可分割的整体,就像一个原子一样,我们把一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性。但实际情况就是不做任何处理的话,在并发情况下CPU进行切换,导致出现原子性的问题,我们一般通过加锁解决,这个不是本文的重点。

Java内存模型真面目

前面讲解并发的三大特性,其中原子性问题可以通过加锁的方式解决,那么可见性和有序性有什么解决的方案呢?其实也很容易想到,可见性是因为缓存导致,有序性是因为编译优化指令重排序导致,那么是不是可以让程序员按需禁用缓存以及编译优化, 因为只有程序员知道什么情况下会出现问题 。 顺着这个思路,就提出了JAVA内存模型(JMM)规范。

Java 内存模型是 Java Memory Model(JMM),本身是一种抽象的概念,实际上并不存在,描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

​默认情况下,JMM中的内存机制如下:

  • 系统存在一个主内存(Main Memory),Java 中所有变量都存储在主存中,对于所有线程都是共享的

  • 每条线程都有自己的工作内存(Working Memory),工作内存中保存的是主存中某些变量的拷贝

  • 线程对所有变量的操作都是先对变量进行拷贝,然后在工作内存中进行,不能直接操作主内存中的变量

  • 线程之间无法相互直接访问,线程间的通信(传递)必须通过主内存来完成

同时,JMM规范了 JVM 如何提供按需禁用缓存和编译优化的方法,主要是通过volatile、synchronized 和 final 三个关键字,那具体的规则是什么样的呢?

JMM 中的主内存、工作内存与 JVM 中的 Java 堆、栈、方法区等并不是同一个层次的内存划分,这两者基本上是没有关系的。

Happens-Before规则

JMM本质上包含了一些规则,那这个规则就是大家有所耳闻的Happens-Before规则,大家都理解了些规则吗?

Happens-Before规则,可以简单理解为如果想要A线程发生在B线程前面,也就是B线程能够看到A线程,需要遵循6个原则。如果不符合 happens-before 规则,JMM 并不能保证一个线程的可见性和有序性。

1.程序的顺序性规则

在一个线程中,逻辑上书写在前面的操作先行发生于书写在后面的操作。

这个规则很好理解,同一个线程中他们是用的同一个工作缓存,是可见的,并且多个操作之间有先后依赖关系,则不允许对这些操作进行重排序。

2. volatile 变量规则

指对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作。

怎么理解呢?比如线程A对volatile变量进行写操作,那么线程B读取这个volatile变量是可见的,就是说能够读取到最新的值。

3.传递性

这条规则是指如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。

这个规则也比较容易理解,不展开讨论了。

  1. 锁的规则

这条规则是指对一个锁的解锁 Happens-Before于后续对这个锁的加锁,这里的锁要是同一把锁, 而且用synchronized或者ReentrantLock都可以。

如下代码的例子:

synchronized (this) { // 此处自动加锁 // x 是共享变量, 初始值 =10 if (this.x < 12) { this.x = 12; } } // 此处自动解锁 复制代码
  • 假设 x 的初始值是 8,线程 A 执行完代码块后 x 的值会变成 12(执行完自动释放锁)

  • 线程 B 进入代码块时,能够看到线程 A 对 x 的写操作,也就是线程 B 能够看到 x==12。

5.线程 start() 规则

主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。

这个规则也很容易理解,线程 A 调用线程 B 的 start() 方法(即在线程 A 中启动线程 B),那么该 start() 操作 Happens-Before 于线程 B 中的任意操作。

6.线程 join() 规则

线程 A 中,调用线程 B 的 join() 并成功返回,那么线程 B 中的任意操作 Happens-Before 于该 join() 操作的返回。

使用JMM规则

我们现在已经基本讲清楚了JAVA内存模型规范,以及里面关键的Happens-Before规则,那有啥用呢?回到前言的问题中,我们是不是可以使用目前学到的关于JMM的知识去解决这个问题。

方案一: 使用volatile

​根据JMM的第2条规则,主线程写了volatile修饰的run变量,后面的t线程读取的时候就可以看到了。

方案二:使用锁

​利用synchronized锁的规则,主线程释放锁,那么后续t线程加锁就可以看到之前的内容了。

小结:

volatile 关键字

  • 保证可见性

  • 不保证原子性

  • 保证有序性(禁止指令重排)

volatile 修饰的变量进行读操作与普通变量几乎没什么差别,但是写操作相对慢一些,因为需要在本地代码中插入很多内存屏障来保证指令不会发生乱序执行,但是开销比锁要小。volatile的性能远比加锁要好。

synchronized 关键字

  • 保证可见性

  • 不保证原子性

  • 保证有序性

加了锁之后,只能有一个线程获得到了锁,获得不到锁的线程就要阻塞,所以同一时间只有一个线程执行,相当于单线程,由于数据依赖性的存在,单线程的指令重排是没有问题的。

线程加锁前,将清空工作内存中共享变量的值,使用共享变量时需要从主内存中重新读取最新的值;线程解锁前,必须把共享变量的最新值刷新到主内存中。

总结

本文讲解了JAVA并发的3大特性,可见性、有序性和原子性。从而引出了JAVA内存模型规范,这主要是为了解决并发情况下带来的可见性和有序性问题,主要就是定义了一些规则,需要我们程序员懂得这些规则,然后根据实际场景去使用,就是使用volatile、synchronized、final关键字,主要final关键字也会让其他线程可见,并且保证有序性。那么具体他们底层的实现是什么,是如何保证可见和有序的,我们后面详细讲解。

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

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

相关文章

基于Java+Swing实现《扫雷》游戏

基于JavaSwing实现《扫雷》游戏一、系统介绍二、功能展示三、其他系统一、系统介绍 windows自带的游戏《扫雷》是陪伴了无数人的经典游戏&#xff0c;本程序参考《扫雷》的规则进行了简化&#xff0c;用java语言实现&#xff0c;采用了swing技术进行了界面化处理&#xff0c;设…

基于蚁群算法求解运钞车路径规划问题(Matlab代码实现)

&#x1f352;&#x1f352;&#x1f352;欢迎关注&#x1f308;&#x1f308;&#x1f308; &#x1f4dd;个人主页&#xff1a;我爱Matlab &#x1f44d;点赞➕评论➕收藏 养成习惯&#xff08;一键三连&#xff09;&#x1f33b;&#x1f33b;&#x1f33b; &#x1f34c;希…

[附源码]JAVA毕业设计桔子酒店客房管理系统(系统+LW)

[附源码]JAVA毕业设计桔子酒店客房管理系统&#xff08;系统LW&#xff09; 目运行 环境项配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目…

vue app开发调用原生方法实现权限访问授权处理(一)

vue app开发调用原生方法实现权限访问授权处理 前提&#xff1a;在写代码之前应该想清楚调用原生安卓、ios的方法&#xff0c;就应该遵循双端的方法规则&#xff0c;调用方法时应该注意&#xff0c;在这里先主要介绍一下注意事项&#xff1a; 根据App发布应用市场的要求&…

【sciter】安全应用列表控件总结

一、效果图 二、功能点 实现电脑文件拖拽进入到安全桌面,读取文件路径,生成应用。可以配置允许拖拽进入安全桌面的文件应用。点击添加图标,可以添加应用到安全桌面中。在安全桌面列表中每一个应用实现双击、失去焦点,获取焦点、右键事件在安全桌面列表中每一个应用可以实现…

[附源码]计算机毕业设计springboot疫情防控平台

项目运行 环境配置&#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…

HTML5期末大作业:用DIV+CSS技术设计的网页与实现(剪纸传统文化网页设计主题)

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

二本土木工程管理毕业5年,零基础转型大数据开发,收割长沙深圳多个大数据offer...

点击上方 "大数据肌肉猿"关注, 星标一起成长点击下方链接&#xff0c;进入高质量学习交流群今日更新| 1052个转型案例分享-大数据交流群分享一位学习群同学的转型经历&#xff0c;他是二本土木类工程管理专业&#xff0c;17年毕业&#xff0c;毕业后在长沙工地从事了…

Java基于springboot+vue的汽车饰品销售购物商城系统 前后端分离

开发背景 随着我国科技和经济的发展&#xff0c;我国的汽车数量也越来越多&#xff0c;基本家家户户都拥有了自己的汽车&#xff0c;为了让汽车用起来更加的舒心&#xff0c;于是各类琳琅满目的汽车饰品也出现了。大多数时候人们在购买汽车饰品的时候都回到这些专卖店购买&…

C++之面向对象

目录 对象与类 类的语法&#xff1a; C中class与struct的区别&#xff1a; 通过类实例化对象的方式 具体案例 类作用域与分文件编写 创建circle.h头文件 创建源文件circle.cpp 创建all.cpp来作为程序的入口 封装 封装的意义 访问权限符 成员属性私有化 优点 具体…

Python解题 - 括号上色(递归)

题目 小艺酱又得到了一堆括号。括号是严格匹配的。现在给括号进行上色。上色有三个要求&#xff1a; 1、只有三种上色方案&#xff0c;不上色&#xff0c;上红色&#xff0c;上蓝色。 2、每对括号只有一个上色。 3、相邻的两个括号不能上相同的颜色&#xff0c;但是可以都不上色…

【Java面试指北】Exception Error Throwable 你分得清么?

读本篇文章之前&#xff0c;如果让你叙述一下 Exception Error Throwable 的区别&#xff0c;你能回答出来么&#xff1f; 你的反应是不是像下面一样呢&#xff1f; 你在写代码时会经常 try catch(Exception)在 log 中会看到 OutOfMemoryErrorThrowable 似乎不常见&#xff0c…

为什么大部分人做网赚是赚不到钱的,这才是真正的原因!

说实话&#xff0c;互联网已经发展到现在的水平&#xff0c;目前来看&#xff0c;互联网上只存在两种平台&#xff0c;一种是社交平台&#xff0c;一种是内容平台。 所有的抖音、知乎、小红书、搜索引擎、淘宝等等这些都是内容平台&#xff0c;如果你想要解决精准流量问题&…

JSP+MySQL基于SSM的高校毕业生就业管理系统

本高校毕业生就业管理系统主要包括系统用户管理模块、招聘信息管理模块、简历接收管理、投递简历管理、登录模块、和退出模块等多个模块。它帮助高校毕业生就业管理实现了信息化、网络化,通过测试,实现了系统设计目标,相比传统的管理模式,本系统合理的利用了高校毕业生就业管理…

配电网重构|基于新颖的启发式算法SOE的随机(SDNR)配电网重构(Matlab代码实现)【算例33节点、84节点、119节点、136节点、417节点】

&#x1f4a5;&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️❤️&#x1f4a5;&#x1f4a5;&#x1f4a5; &#x1f4dd;目前更新&#xff1a;&#x1f31f;&#x1f31f;&#x1f31f;电力系统相关知识&#xff0c;期刊论文&…

【数据集NO.3】人脸识别数据集汇总

文章目录前言一、IMDB-WIKI人脸数据集二、WiderFace人脸检测数据集三、GENKI 人脸图像数据集四、哥伦比亚大学公众人物脸部数据库五、CelebA人脸数据集六、美国国防部人脸库七、MTFL人脸识别数据集八、BioID人脸数据集九、PersonID人脸识别数据集十、CMU PIE人脸库十一、Youtub…

Linux虚拟内存

问题 什么是虚拟内存地址 &#xff1f;Linux 内核为啥要引入虚拟内存而不直接使用物理内存 &#xff1f;虚拟内存空间到底长啥样&#xff1f;内核如何管理虚拟内存&#xff1f;什么又是物理内存地址 &#xff1f;如何访问物理内存&#xff1f; 什么是虚拟内存地址 举一个生活…

Redis学习笔记(四)

事务 一个命令执行的队列&#xff0c;中间不会被打断或者干扰基本操作、 开启事务&#xff1a;multi 作用&#xff1a;设定事务的开启位置&#xff0c;执行此命令后&#xff0c;后续所有指令均加入事务中 执行事务&#xff1a;exec 作用&#xff1a;设定事务结束的位置&#xf…

【MySQL】表的增删改查(一)

你可以了解世间万物&#xff0c;但追根溯源的唯一途径便是亲身尝试。——《心灵捕手》 前言&#xff1a; 大家好&#xff0c;我是拳击哥&#xff0c;今天给大家讲解的是mysql表GRUD操作中的新增数据、查询数据以及表中数据的排序、去重等。因篇幅过长&#xff0c;分为两期来讲解…

Linux——匿名管道、命名管道及进程池概念和实现原理

目录 一.什么是匿名管道 二.如何使用匿名管道 &#xff08;一&#xff09;.pipe原理 &#xff08;二&#xff09;.pipe使用 三.命名管道概念及区别 &#xff08;一&#xff09;.什么是命名管道 &#xff08;二&#xff09;.与匿名管道的联系和区别 四.命名管道的使用 &…