深入理解Java关键字volatile

前置知识-了解以下CPU结构

如下图所示,每个CPU都会有自己的一二级缓存,其中一级缓存分为数据缓存指令缓存,这些缓存的数据都是从内存中读取的,而且每次都会加载一个cache line,关于cache line的大小可以使用命令cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size 进行查看。

在这里插入图片描述

如果开发者能够很好的使用缓存技术,那么程序的性能就会很高,具体可以参照笔者之前写的这篇文章

计算机组成原理-基于计组CPU的基础知识进行代码调优

在这里插入图片描述

CPU Cache和内存同步技术

写直达(Write Through)

写直达技术解决cache和内存同步问题的方式很简单,例如CPU1要操作变量i,先看看cache中有没有变量i,若有则直接操作cache中的值,然后立刻写回内存。
若变量i不在cache中,那么CPU就回去内存中加载这个变量到cache中进行操作,然后立刻写回内存中。
这样做的好处就是实现简单,缺点也很明显,因为每次都要将修改的数据立刻写回内存。

在这里插入图片描述

写回(Write Back)

这种方式相较于上者来说性能相对较好一些,举个例子,CPU1要修改变量i,假如变量i在cache中,在修改完之后我们就将这个Cache Block 为脏(Dirty),意味这个数据被改过了和内存不一样。但此时此刻我们不会讲数据写回内存中。
CPU CACHE需要加载别的变量时,发现这个变量使用的cache block就是变量i的内存空间时,我们再将变量i的值写回内存中。

在这里插入图片描述

CPU缓存一致性问题(Intel处理器的实现)

由上图我们知道,当一台计算机由多核CPU构成的时候,每个CPU都从内存里加载对应的变量i(假设变量i初值为0)将其改为100,却没有写回内存。
这时候CPU2再去内存从取变量i,进行+1,因为CPU1修改的值没有写回内存,所以CPU2操作的变量i最终结果是0+1=1,这就是经典的缓存一致性问题。
而解决这个问题我们只要攻破以下两点问题即可:

  1. 写传播问题:即当前Cache中修改的值要让其他CPU知道
  2. 事务串行化:例如CPU1先将变量i改为100,CPU2再将基于当前变量i的值乘2。我们必须保证变量i先加100,再乘2。

在这里插入图片描述

解决缓存一致性问题方案1——总线嗅探(Bus Snooping)

总线嗅探是解决写传播的解决方案,举个例子,当CPU1更新Cache中变量i的值时,就会通知其他核心变量i的值被它改了,当其他CPU发现自己Cache中也有这个值的时候就会将CPU1cache的结果更新到自己的cache中。
这种方式缺点很明显,CPU必须无时不刻监听变化,而且出现变化的数据自己还不一定有,这样的作法增加了总线的压力。

而且也不能保证事务串行化,如下图,CPUA加载了变量修改了值通知其他CPU这个值有变化了。
而CPUB也改了i的值,按照正常的逻辑CPUC、CPUD的值应该是先变为100在变为200。
但是CPUC先收到CPUB的通知先改为200再收到CPUA的通知变为100,这就导致的数据不一致的问题,即事务串行化失败。

在这里插入图片描述

解决缓存一致性问题方案2——MESI协议

MESI是总线嗅探的改良版,他很好的解决了总线的带宽压力,以及很好的解决了数据一致性问题。
在介绍MESI之前,我们必须了解以下MESI是什么。

  1. M(Modified,已修改)MESI第一个字母M,代表着CPU当前L1 cache中某个变量i的状态被修改了,而且这个数据在其他核心中都没有。
  2. E(Exclusive,独占),说白了就是CPUA将数据加载自己的L1 cache时,其他核心的cache中并没有这个数据,所以CPUA将这个数据加载到自己的cache时标记为E。
  3. (S:Shared,共享):说明CPUA在加载这个数据时,其他CPU已经加载过这个数据了,这时CPUA就会从其他CPU中拿到这个数据并加载到L1 cache中,并且所有拥有这个值的CPU都会将cache中的这个值标记为S。
  4. (I:Invalidated,已失效):当CPUA修改了L1 cache中的变量i时,发现这个值是S即共享的数据,那么就需要通知其他核心这个数据被改了,其他CPU都需要将cache中的这个值标为I,后面要操作的时,必须拿到最新的数据在进行操作。

好了介绍完这几个状态之后,我们不妨用一个例子过一下这个流程:

  1. CPUA要加载变量i,发现变量i不在cache中,于是去内存中捞数据,此时通过总线发个消息给其他核心,其他核心的cache中并没有这条数据,所以这个变量的cache中的状态为E(独占)。
  2. CPUB也加载这个数据了,在总线上发了个消息,发现CPUA有这个数据且并没有修改或者失效的标志,于是他们一起将这个变量i状态设置为S(共享)
  3. CPUA要改变量i值了,发消息给其他核心,其他核心收到消息将自己的变量i设置为I(无效),CPUA改完后将数据设置为M(已修改)
  4. CPUA又要改变量i的值了,而且看到变量i的状态为M(已修改),说明这个值是最新的数据,所以不发消息给其他核心了,直接更新即可。
  5. CPUA要加载新的变量x了,而且变量x要使用的cache空间正是变量i的,所以CPUA将值写回内存中,这时候内存和最新数据同步了。

将其翻译成状态机,如下所示

在这里插入图片描述

volatile关键字

基于JMM模型从语言级模型上了解可见性的原理

这里需要提前说明一下JMM内存模型并非我们平时所认为的内存模型,而是Java为程序员提供的抽象级别内存模型,让程序员屏蔽底层的硬件细节。
我们都知道volatile是可以保证变量可见性的,从JMM提供的抽象模型上来说,它保证可见性的方式很简单,JMM模型工作机制如下,假设我们有一个volatile共享变量a,值为1,线程1和线程2协作的操作如下

  1. 由于是volatile变量,所以线程1就不会对应将本地内存设置为无效,直接从主存中获取,并加载到主存中。
  2. 然后线程1将值修改为2,由于需要保证可见性,所以JMM会把这个变量写如主存中。
  3. 线程2读取,同样因为volatile修饰的原因,不走本地内存,直接从主存中获取,从而保证缓存一致性。

在这里插入图片描述

从硬件层面理解volatile(就Intel处理器而言)

笔者上面说过了,JMM是语言级内存模型,即让程序员了解逻辑原理的内存模型,JMM规范了volatile关键字具备这种特性,真正底层是实现并非如此。

Intel处理器而言volatile关键字就是基于我们上文所说的MESI协议。声明为volatile的变量每当被一个线程修改后,就会通知其他线程该变量状态为无效(参考上面MESI协议设置为Invalid标记),此时其他线程若需要对这个变量进行进一步操作就需要重新去主存中读取了。

当然如果是AMD可能就另当别论了。

volatile保证可见性代码示例

代码如下所示,读者可以试试看加上num变量volatile和删除volatile的效果

public class VolatileModify {private volatile   static int num = 0;public static void main(String[] args) {Thread t1 = new Thread(() -> {while (num == 0) {//注意不要加这句话,实验会失败
//                System.out.println("循环中");}System.out.println("num已被修改为:1" );});Thread t2 = new Thread(() -> {try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}num++;System.out.println("t2修改num为1" );});t1.start();t2.start();}}

这里笔者直接给出结论,加上volatile后,这两个线程实际上会做这几件事(我们假设这两个线程在不同CPU上):

1. 线程1获取共享变量num的值,此时并没有其他核心上的线程获取,状态为E。
2. 线程2启动也获取到num的值,此时总线嗅探到另一个CPU也有这个变量的缓存,所以两个CPU缓存行都设置为S。
3. 线程2修改num的值,通过总线嗅探机制发起通知,线程1的线程收到消息后,将缓存行变量设置为I。
4. 线程1下次在读取数据是从主存中读取,所以就结束死循环。

输出结果

 /*** 加volatile关键字* t2修改num为1* num已被修改为:1*/

而不加volatile,则t1无法感知改变就会一直走CPU Cache中的值,导致死循环。

  /*** 不加volatile*t2修改num为1*/

volatile确保禁止指令重排序

关于指令重排序可以参考笔者编写的这篇文章

Java内存模型(JMM)详解

volatile不仅可以保证可见性,还可以避免指令重排序,我们不妨看一段双重锁校验的单例模式代码,代码如下所示可以看到经过双重锁校验后,会进行new Singleton();

public class Singleton {private volatile static Singleton uniqueInstance;private Singleton() {}public  static Singleton getUniqueInstance() {//先判断对象是否已经实例过,没有实例化过才进入加锁代码if (uniqueInstance == null) {//类对象加锁synchronized (Singleton.class) {if (uniqueInstance == null) {uniqueInstance = new Singleton();}}}return uniqueInstance;}
}

这一操作,这个操作乍一看是原子性的,实际上编译后再执行的机器码会将其分为3个动作:

1. 为引用uniqueInstance分配内存空间
2. 初始化uniqueInstance
3. uniqueInstance指向分配的内存空间

所以如果没有volatile 禁止指令重排序的话,1、2、3的顺序操作很可能变成1、3、2进而导致一个线程执行1、3创建一个未初始化却不为空的对象,而另一个线程判空操作判定不空直接返回出去,从而导致执行出现异常。

在这里插入图片描述

volatile无法保证原子性

无法保证原子性代码示例

我们不妨看看下面这段代码,首先我们需要了解一下num++这个操作在底层是如何实现的:

	1. 读取num的值2. 对num进行+13. 写回内存中

我们查看代码的运行结果,可以看到最终的值不一定是10000,由此可以得出volatile并不能保证原子性

public class VolatoleAdd {private static int num = 0;public void increase() {num++;}public static void main(String[] args) {int size = 10000;CountDownLatch downLatch = new CountDownLatch(1);ExecutorService threadPool = Executors.newFixedThreadPool(size);VolatoleAdd volatoleAdd = new VolatoleAdd();for (int i = 0; i < size; i++) {threadPool.submit(() -> {try {downLatch.await();} catch (InterruptedException e) {e.printStackTrace();}volatoleAdd.increase();});}downLatch.countDown();threadPool.shutdown();while (!threadPool.isTerminated()) {}System.out.println(VolatoleAdd.num);//9998}
}

保证原子性的解决方案

  1. synchronized
public synchronized void increase() {num++;}
  1. 原子类
private static AtomicInteger num=new AtomicInteger(0);public void increase() {num.getAndIncrement();}
  1. Lock
 Lock lock = new ReentrantLock();public void increase() {lock.lock();try {num++;} finally {lock.unlock();}}

你有那些场景用到了volatile嘛?

我们希望从视频中切割中图片进行识别,只要有一张图片达到90分就说明本次业务流程是成功的,为了保证执行效率,我们把截取的每一张图片都用一个线程去进行识别分数。
为了做到一张识别90分就结束任务,我们在task内部定义一个变量private CountDownLatch countDownLatch;,为了保证识别到一张图片将识别结果自增为1,我们使用了原子类private volatile AtomicInteger atomicInteger ;,可以看到,笔者为了保证原子类多线程下的可见性加了volatile

完整代码如下

public class Task implements Callable<Integer> {private static Logger logger = LoggerFactory.getLogger(Task.class);private volatile AtomicInteger atomicInteger ;private CountDownLatch countDownLatch;public Task(AtomicInteger atomicInteger, CountDownLatch countDownLatch) {this.atomicInteger = atomicInteger;this.countDownLatch = countDownLatch;}@Overridepublic Integer call() throws Exception {int score = (int) (Math.random() * 100);logger.info("当前线程:{},识别分数:{}", Thread.currentThread().getName(), score);synchronized (this){}if (score > 90 && atomicInteger.getAndIncrement()==0) {logger.info("当前线程:{} countDown",Thread.currentThread().getName());countDownLatch.countDown();logger.info("当前线程:{} 返回比对分数:{}", Thread.currentThread().getName(), score);return score;}return -1;}
}

测试代码

public class Main {private static Logger logger = LoggerFactory.getLogger(Main.class);public static void main(String[] args) throws InterruptedException {ExecutorService threadPool = Executors.newFixedThreadPool(100);CountDownLatch countDownLatch=new CountDownLatch(1);AtomicInteger atomicInteger=new AtomicInteger(0);for (int i = 0; i < 100; i++) {Future<Integer> task = threadPool.submit(new Task(atomicInteger,countDownLatch));}logger.info("阻塞中");countDownLatch.await();logger.info("阻塞结束");threadPool.shutdown();while (!threadPool.isTerminated()){}}
}

参考文献

CPU 缓存一致性

volatile可见性实现原理

吃透Java并发:volatile是怎么保证可见性的

volatile 三部曲之可见性

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

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

相关文章

stm32与Freertos入门(二)移植FreeRTOS到STM32中

简介 注意&#xff1a;FreeRTOS并不是实时操作系统&#xff0c;而是分时复用的&#xff0c;只不过切换频率很快&#xff0c;感觉上是同时在工作。本次使用的单片机型号为STM32F103C8T6,通过CubeMX快速移植。 一、CubeMX快速移植 1、选择芯片 打开CubeMX软件&#xff0c;进行…

(独白)我为什么选择了计算机行业?

为什么可能很简单&#xff0c;但为什么的为什么就有点长了。就当作讲故事吧 在高中毕业后选择专业时&#xff0c;和大多数人一样&#xff0c;我根本不知道要选择什么专业&#xff0c;更不知道哪个专业发展前景好&#xff0c;哪个专业好就业。在当时比较火的专业我记得应该是土…

使用广播星历进行 GPS 卫星位置的计算

目录 1.计算卫星运动的平均角速度 n 2.计算观测瞬间卫星的近地点角 3.计算偏近点角 4.计算真近点角 f 5.计算升交角距 6.计算摄动改正项 7.进行摄动改正 8.计算卫星在轨道面坐标系中的位置 9.计算观测瞬间升交点的经度 L 10.计算卫星在瞬时地球坐标系中的位置 11.…

JDK21+HADOOP3.2.2+Windows安装步骤

哈哈哈 最近转战大数据这块了&#xff0c;分享一下hadoop3.2.2的安装步骤 借鉴了不少大佬的文章&#xff0c;如有雷同&#xff0c;都是大佬们的 1.JDK安装 我选择的是JDK21 以下是下载网址和截图&#xff0c;这个没有太多的&#xff0c;一般下载最新的就可以 JDK: Java Down…

SQL数列

SQL数列 1、数列概述2、SQL数列2.1、简单递增序列2.2、等差数列2.3、等比数列3、SQL数列的应用3.1、连续问题3.2、多维分析1、数列概述 数列是最常见的数据形式之一,实际数据开发场景中遇到的基本都是有限数列。常见的数列例如:简单递增序列、等差数列、等比数列等 SQL如何实…

Helplook VS Salesforce:哪个知识库更好?

对于组织来说&#xff0c;选择一个合适的平台来管理在线知识库可能是一个具有挑战性的任务。而Salesforce的知识管理功能可以帮助组织更好地管理和分享他们的知识&#xff0c;从而更好地为客户提供服务。这是一种将知识管理集成到CRM平台中的方法&#xff0c;可以简化知识共享和…

vue el-cascader组件change失效以及下拉框不消失的问题

文章目录 1.前言2. 碰到的问题3. 如何解决这两个问题 1.前言 最近项目上用到el-cascader这个组件,需要可以选第一级菜单&#xff0c;也需要可以选第二级菜单&#xff0c;点击完成之后需要关闭下拉框。其实功能比较简单&#xff0c;找了很多资料&#xff0c;没有找到合适的方案…

分类预测 | GASF-CNN格拉姆角场-卷积神经网络的数据分类预测

分类预测 | GASF-CNN格拉姆角场-卷积神经网络的数据分类预测 目录 分类预测 | GASF-CNN格拉姆角场-卷积神经网络的数据分类预测分类效果基本描述模型描述程序设计参考资料 分类效果 基本描述 1.GASF-CNN格拉姆角场-卷积神经网络的数据分类预测&#xff08;完整源码和数据) 2.自…

音乐制作软件Ableton Live 11 mac功能特点

Ableton Live 11 mac是一款数字音频工作站软件&#xff0c;用于音乐制作、录音、混音和现场演出是一款流行的音乐制作软件。 Ableton Live 11 mac特点和功能 Comping功能&#xff1a;Live 11增加了Comping功能&#xff0c;允许用户在不同的录音轨道上进行多次录音&#xff0c;…

springboot自定义starter步骤

引入相关依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional> </dependency><dependency><groupId>org.pro…

《Java已死、前端已凉》:真相与焦虑的辩证

文章目录 Java 企业级支柱Java 在企业级应用中的地位后端开发的支柱Java生态系统的强大 前端&#xff1a;蓬勃发展的创新引擎新技术的涌现用户体验的重要性 Java的演进与创新云原生时代的 Java开发效率和生态系统 前端技术的未来走向WebAssembly 的崛起可访问性和国际化的重要性…

中海达亮相能源北斗与时空智能创新技术应用大会

12月7日-8日&#xff0c;2023年能源北斗与时空智能创新技术应用大会暨鹭岛论坛在厦门举办。本次活动以“能源北斗时空智能”为主题&#xff0c;由中关村智能电力产业技术联盟、中国能源研究会、中国卫星导航定位协会、中国电力科学研究院有限公司、国网信息通信产业集团有限公司…

Java学习之面向对象

一、面向对象 1、引入面向对象 方法中封装的是具体实现某一功能的代码&#xff0c;而通过书写一个拥有多个特定方法的类&#xff0c;来存放的就是一个又一个的方法。 方法都存放在类里面&#xff0c;当需要使用的时候&#xff0c;不用去找具体的方法&#xff0c;而是先找这个…

什么是前端响应式设计(responsive design)?如何实现响应式布局?

聚沙成塔每天进步一点点 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发…

2、快速搞定Kafka术语

快速搞定Kafka术语 Kafka 服务端3层消息架构 Kafka 客户端Broker 如何持久化数据小结 Kafka 服务端 3层消息架构 第 1 层是主题层&#xff0c;每个主题可以配置 M 个分区&#xff0c;而每个分区又可以配置 N 个副本。第 2 层是分区层&#xff0c;每个分区的 N 个副本中只能有…

操作系统中的作业管理

从用户的角度看&#xff0c;作业是系统为完成一个用户的计算任务&#xff08;或一次事务处理&#xff09;所做的工作总和。例如&#xff0c;对于用户编制的源程序&#xff0c;需经过对源程序的编译、连接编辑或连接装入及运行产生计算结果。这其中的每一个步骤&#xff0c;常称…

laravel的安装

laravel的安装&#xff08;Composer小皮&#xff09; Composer的安装 windows下安装 https://getcomposer.org/Composer-Setup.exe 修改镜像 阿里云&#xff1a; composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/ 华为云&#xff1a; compos…

机器学习---KNN最近邻算法

1、KNN最近邻算法 K最近邻(k-Nearest Neighbor&#xff0c;KNN)分类算法&#xff0c;是一个理论上比较成熟的方法&#xff0c;也是最简单的机器学习算法之一&#xff0c;有监督算法。该方法的思路是&#xff1a;如果一个样本在特征空间中的k个最相似的样本中的大多数属于某一个…

深度学习 Day12——P1实现mnist手写数字识别

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f356; 原作者&#xff1a;K同学啊 | 接辅导、项目定制 文章目录 前言1 我的环境2 代码实现与执行结果2.1 前期准备2.1.1 引入库2.1.2 设置GPU&#xff08;如果设备上支持GPU就使用GPU,否则使用C…

React中的setState执行机制

我这里今天下雨了&#xff0c;温度一下从昨天的22度降到今天的6度&#xff0c;家里和学校已经下了几天雪了&#xff0c;还是想去玩一下的&#xff0c;哈哈&#xff0c;只能在图片里看到了。 一. setState是什么 它是React组件中用于更新状态的方法。它是类组件中的方法&#x…