22.Volatile原理

文章目录

  • Volatile原理
    • 1.Volatile语义中的内存屏障
      • 1.1.volatile写操作的内存屏障
        • 1.1.1.StoreStore 屏障
        • 1.1.2.StoreLoad 屏障
      • 1.2.volatile读操作的内存屏障
        • 1.2.1.LoadStore屏障
        • 1.2.2.LoadLoad屏障
    • 2.volatile不具备原子性
        • 2.1.原理

Volatile原理

1.Volatile语义中的内存屏障

在Java代码中,volatile关键字主要又两层语义

  1. 不同线程对volatile变量的值具有内存可见性,就是一个线程修改了某个volatile变量的值,该值对其他线程立即可见。
  2. 禁止指令重排序

同时volatile关键字不仅能保证可见性,还能保证有序性,保证有序性是通过内存屏障指令来确保的。

JVM编译器会在生成字节码文件时,会在指令序列中插入内存屏障来禁止特定类型的CPU重排序。

JVM在处理volatile关键字修饰的变量时,会采取保守策略来确保内存可见性和有序性,这涉及到内存屏障(Memory Barrier)的使用。内存屏障是一种硬件层面的指令,用于确保某些内存访问操作的执行顺序,防止CPU的乱序执行对并发程序的正确性产生影响。对于volatile变量的读写,JVM会分别在读和写操作前后插入适当类型的内存屏障,确保以下几点:

  1. 全局可见性: 保证对volatile变量的写操作能立即对其他线程可见,即使在不同的CPU缓存中也是如此。这意味着写操作之后,修改的值会立刻刷出到主内存中。
  2. 禁止重排序: 防止编译器和CPU对涉及volatile变量的代码进行不必要的重排序,确保它们按照程序员指定的顺序执行。这对于依赖于特定顺序的并发控制逻辑至关重要。

基于保守策略的volatile操作内存屏障插入策略主要包括以下方面:

  • 写屏障(Store Memory Barrier): 在写入volatile变量之后插入。它的作用是确保在该屏障之前的所有普通写操作(非volatile)都已完成,并且将当前线程的工作内存中的volatile变量值刷新到主内存中。这样,任何读取该volatile变量的线程都能看到最新值。
    • 在每个volatile操作面插入一个StoreStore屏障
    • 在每个volatile操作面插入一个 StoreLoad屏障
  • 读屏障(Load Memory Barrier): 在读取volatile变量之前插入。它的作用是确保读取操作之后的加载不会被重排序到该屏障之前,并且使CPU读取主内存中的最新值,而不是使用缓存中的旧值,从而确保读取到的是最近一次写入的值,无论这个写入操作发生在哪个线程中。
    • 在每个volatile操作面插入一个LoadLoad屏障
    • 在每个volatile操作面插入一个 LoadStore屏障

这些屏障的联合使用确保了对volatile变量的读写操作具有原子性和全局有序性,尽管它们不保证复合操作(如count++)的原子性。这就是为什么即使在没有锁的情况下,volatile也能作为轻量级的同步机制,用于状态标记、双重检查锁定模式等场景。

1.1.volatile写操作的内存屏障

volatile写操作的内存屏障插入策略为:在每个volatile写操作前插入SotreStore(SS)屏障,在写操作之后加上StoreLoad屏障

1.1.1.StoreStore 屏障

定义与作用StoreStore屏障主要用于确保一个存储(写)操作在另一个存储操作之前完成。换句话说,它强制所有在该屏障之前的存储操作在该屏障之后的存储操作之前完成。这种屏障通常用于避免写-写冲突导致的数据不一致问题,尤其是在处理器有乱序执行能力的体系结构中。

  • 前面的写入不会重排序到后面
  • 前面的写指令完成后,高速缓存数据刷入主存
  • 后面的写操作不会排序到前面

应用场景: 例如,在实现某些类型的锁释放操作时,可能需要确保解锁操作前的所有写操作已经完成,以免新获得锁的线程看到不一致的状态。

1.1.2.StoreLoad 屏障

定义与作用StoreLoad屏障是Java内存模型中最强大的一种屏障,它确保在屏障之前的所有写操作(存储操作)在屏障之后的任何读操作(加载操作)之前完成。这意味着不仅要求写操作完成,而且要确保这些写操作对所有线程可见。因此,StoreLoad屏障通常用于实现volatile变量的写操作后,以确保写入的值对其他线程立即可见。

  • 前面的写不会重排序到后面
  • 前面的写指令操作完成后,高速缓存数据立即刷入主存
  • 让高速缓存的数据失效,重新从主存中加载数据,保证内核的高速缓存数据一致
  • 后面的读操作不会重排序到前面

应用场景StoreLoad屏障直接关联于Java中volatile字段的写操作实现。当一个线程修改了一个volatile变量的值,JVM会在写操作之后插入一个StoreLoad屏障,以确保该写入的值能够立即对其他线程可见,同时刷新处理器的缓存,避免数据的脏读。此外,它也常用于锁释放操作后的内存可见性保障,确保解锁前的内存更改对后续可能获得锁的线程是可见的。

总结来说,StoreStore屏障关注于维持存储操作之间的顺序,而StoreLoad屏障则进一步确保了写操作的全局可见性,并在写-读操作间建立了一个顺序关系,这对于维护多线程程序的一致性和正确性至关重要。

在这里插入图片描述

1.2.volatile读操作的内存屏障

volatile读操作的内存屏障插入策略为:在每个volatile读操作后面插入LoadLoad(LL)屏障和LoadStore屏障,禁止后面的普通读、普通写、和前面的volatile读操作发生重排序

1.2.1.LoadStore屏障

定义与功能LoadStore屏障,也称为读写屏障,其主要作用是确保屏障之后的读操作不会被重排序到屏障之前,且屏障之后的写操作不会被重排序到屏障之前的读操作之前。这意味着它不仅确保了读操作不会提前,还阻止了读之后的写操作与读操作之前的任何写操作发生乱序。这在volatile读操作的上下文中,意味着确保了读取到的volatile变量的值不会被之后的写操作所覆盖或影响,保持了读取操作的确定性。

  • 前面的读操作不会排到后面
  • 让高速缓存中的数据失效,重新从主存中加载数据
  • 后面的写操作不会排列到前面

在volatile读操作中的应用: 当执行volatile读操作时,Java虚拟机(JVM)会在读取操作之后插入一个LoadStore屏障。这个屏障的目的是确保当前线程的任何后续写操作不会与刚完成的volatile读操作交错,保证了volatile读的值不会因为之后的写而变得无效或不一致。同时,这也间接帮助确保了volatile读取操作后的写操作不会与之前的volatile读操作或普通读操作发生冲突,维护了操作的顺序性。

1.2.2.LoadLoad屏障

定义与功能LoadLoad屏障,或称为读读屏障,它的主要职责是防止屏障之后的读操作被重排序到屏障之前的任何读操作之前。这意味着它确保了在屏障之后执行的任何读操作不会因为CPU的乱序执行优化而提前到屏障之前执行。尽管LoadLoad屏障本身在某些JMM的描述中不常直接提及,但讨论内存屏障时,其概念往往隐含在维护读操作顺序性的讨论中。

  • 前面的读操作不会被排到后面
  • 让高速缓存中的数据失效,重新从主存中加载数据
  • 后面读操作不会排列到前面

在volatile读操作中的应用: 虽然直接提及LoadLoad屏障在volatile读操作后插入的情况较少见,通常强调的是LoadStoreStoreLoad屏障的作用,但理解其概念对于全面把握内存屏障如何维护顺序性是有帮助的。在volatile读操作的上下文中,可以抽象理解为,屏障的逻辑效果确保了读取volatile变量的值不会被之后的其他读取操作提前,保证了读取volatile变量的顺序性。不过,实际中,volatile读操作的关键在于通过StoreLoad屏障确保了对其他线程写入volatile变量的值立即可见,同时防止了volatile读与普通读写操作的不恰当重排序。

在这里插入图片描述

2.volatile不具备原子性

volatile能保证数据的可见性,但是volatile并不能完全保证数据的原子性。对于volatile类型的变量进行符合操作例如(i++),仍然会存在线程不安全的问题

/*** 使用 10个线程,每个线程进行1000次 ++操作,来观察成员变量的结果是否符合我们的预期*/
public class VolatileAddDemo {private volatile int num = 0;@Test@DisplayName("测试并发情况下 volatile原子性")public void testVolatileAdd() {CountDownLatch latch = new CountDownLatch(10);ExecutorService executorService = Executors.newFixedThreadPool(10);for (int i = 0; i < 10; i++) {executorService.submit(() -> {for (int j = 0; j < 1000; j++) {num++;}// 每次执行完毕 -1latch.countDown();});}// 等待全部线程执行完毕try {latch.await();} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println("最终的结果:" + num);System.out.println("预期的结果:10000");System.out.println("相差:" + (10000 - num));}}

在这里插入图片描述

2.1.原理

首先来看一下JMM对变量进行读取和写入的操作流程

在这里插入图片描述

对于非volatile修饰的普通变量来说,在读取变量的时候,JMM要求需要 报纸readload的顺序即可

但是,从主存中读取 x 、y 两个变量的值,可能的操作是 read x -> read y -> load y -> load x,它并不要求操作是连续的

对于关键字 volatile修饰的内存可见变量而言,具有两个重要的语义

  1. 使用volatile修饰的变量在变量值发生改变时,会立即同步到主存,并且让其他线程的变量副本失效
  2. 禁止指令重排序:用volatile修饰的变量在硬件层面上会通过在指令前后加入内存屏障来实现,编译器通过以下规则来进行实现的。
    • 使用volatile修饰的变量 **read(读取),load(加载),use(使用)**都是连续出现的,所以每次使用变量时都要从主存读取最新的变量值,替换私有内存的变量副本值
    • 对于同一个变量的**assign(赋值),store(存储),write(主存)**操作都是连续出现,所以每次对变量的修改都会立即同步到主存中

?但是思考一下,单线程下**( read,load,use)(assign,store,write)**同时出现没什么问题,但是在多线程并发执行的情况下,因为单个操作具备原子性,但是多个组合的话就不具备原子性了,还是有可能会出现脏数据。

下面通过图来了解一下 并发时可能发生产生脏数据的场景

在这里插入图片描述

对于复合操作,volatile变量是无法保证其原子性的,如果想要保证复合操作的原子性,那么就需要使用锁,并在在高并发场景下,volatile变量一定要和Java显示锁结合使用

这里补充介绍一下 JMM内存模型的 8个 操作

操作描述作用的对象
read读取把一个变量的值从主内存或高速缓存读到线程的工作内存中,准备下一步的load操作
load加载入把read操作从主内存读取的变量值放入线程的工作内存中的变量副本中,此时变量才对线程可见
use使用把工作内存中变量的值传递给执行引擎,作为运算的输入
assign赋值把执行引擎计算出的结果赋值给工作内存中的变量
store存储把工作内存中修改后的变量值写回到主内存中
write写出把store操作从工作内存中变量的值写入到主内存,使得其他线程可见
lock加锁作用于主内存的变量,标记变量为线程独占,确保同一时刻只有一个线程能执行lock和unlock之间的操作
unlock解锁释放锁,作用于主内存的变量,允许其他线程获取该变量的锁并进行操作

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

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

相关文章

APM2.8如何做加速度校准

加速度的校准建议准备一个六面平整&#xff0c;边角整齐的方形硬纸盒或者塑料盒&#xff0c;如下图所示&#xff0c;我们将以它作为APM校准时的水平垂直姿态参考&#xff0c;另外当然还需要一块水平的桌面或者地面 首先用双面泡沫胶或者螺丝将APM主板正面向上固定于方形盒子上&…

JavaScrip原型对象

参考 JavaScrip原型对象 | LogDicthttps://www.logdict.com/archives/javascripyuan-xing-mo-shi

每天写两道(二)LRU缓存、

146.LRU 缓存 . - 力扣&#xff08;LeetCode&#xff09; 请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。 实现 LRUCache 类&#xff1a; LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存int get(int key) 如果关键字 key 存在于缓存…

Revit——(2)模型的编辑、轴网和标高

目录 一、关闭缩小的隐藏窗口 二、标高&#xff08;可创建平面&#xff0c;其他标高线复制即可&#xff09; 三、轴网 周围的四个圈和三角表示四个里面&#xff0c;可以移动&#xff0c;不要删除 一、关闭缩小的隐藏窗口 二、标高&#xff08;可创建平面&#xff0c;其他标…

深入分析 Android Activity (二)

文章目录 深入分析 Android Activity (二)1. Activity 的启动模式&#xff08;Launch Modes&#xff09;1.1 标准模式&#xff08;standard&#xff09;1.2 单顶模式&#xff08;singleTop&#xff09;1.3 单任务模式&#xff08;singleTask&#xff09;1.4 单实例模式&#xf…

利用边缘计算网关的工业设备数据采集方案探讨-天拓四方

随着工业4.0时代的到来&#xff0c;工业设备数据采集成为了实现智能制造、提升生产效率的关键环节。传统的数据采集方案往往依赖于中心化的数据处理方式&#xff0c;但这种方式在面对海量数据、实时性要求高的工业场景时&#xff0c;往往显得力不从心。因此&#xff0c;利用边缘…

二叉树的链式结构(二叉树)与顺序结构(堆)---数据结构

一、树的概念与结构 1、树的概念 树是一种非线性的数据结构&#xff0c;它是由n&#xff08;n>0&#xff09;个有限结点组成一个具有层次关系的集合。我们常把它叫做树&#xff0c;是因为它看起来像一棵倒挂的树&#xff0c;它的根是朝上的&#xff0c;而叶是朝下的。 下面…

每日复盘-20240528

今日重点关注&#xff1a; 20240528 六日涨幅最大: ------1--------300956--------- 英力股份 五日涨幅最大: ------1--------301361--------- 众智科技 四日涨幅最大: ------1--------301361--------- 众智科技 三日涨幅最大: ------1--------301361--------- 众智科技 二日涨…

联邦和反射器实验

拓扑图 一.实验要求 1.AS1存在两个环回&#xff0c;一个地址为192.168.1.0/24&#xff0c;该地址不能在任何协议中宣告 AS3存在两个环回&#xff0c;一个地址为192.168.2.0/24&#xff0c;该地址不能在任何协议中宣告 AS1还有一个环回地址为10.1.1.0/24&#xff…

[C][动态内存分配][柔性数组]详细讲解

目录 1.动态内存函数的介绍1.malloc2.free2.calloc4.realloc 2.常见的动态内存错误3.C/C程序的内存开辟4.柔性数组1.是什么&#xff1f;2.柔性数组的特点3.柔性数组的使用4.柔性数组的优势 1.动态内存函数的介绍 1.malloc 函数原型&#xff1a;void* malloc(size_t size)功能…

【PB案例学习笔记】-11动画显示窗口

写在前面 这是PB案例学习笔记系列文章的第11篇&#xff0c;该系列文章适合具有一定PB基础的读者。 通过一个个由浅入深的编程实战案例学习&#xff0c;提高编程技巧&#xff0c;以保证小伙伴们能应付公司的各种开发需求。 文章中设计到的源码&#xff0c;小凡都上传到了gite…

颈源性头痛症状及表

颈源性头痛一般表现为&#xff0c;就是说从枕后一直颞侧&#xff0c;到太阳穴附近&#xff0c;这个是枕小的一个疼痛&#xff0c;还有一部分人从枕后&#xff0c;沿着一个弧线&#xff08;如下图&#xff09;的轨迹到了前额&#xff0c;到我们前额&#xff0c;这样一个疼痛&…

ADIL简单测试实例

参考资料&#xff1a;https://blog.csdn.net/geyichongchujianghu/article/details/130045373这个连接是Java的代码&#xff0c;我根据它的链接写了一个kotlin版本的。 AIDL&#xff08;Android Interface Definition Language&#xff09;是Android平台上用于进程间通信&…

AI办公自动化:kimi批量新建文件夹

工作任务&#xff1a;批量新建多个文件夹&#xff0c;每个文件夹中的年份不一样 在kimi中输入提示词&#xff1a; 你是一个Python编程专家&#xff0c;要完成一个编写关于录制电脑上的键盘和鼠标操作的Python脚本的任务&#xff0c;具体步骤如下&#xff1a; 打开文件夹&…

FFmpeg编解码的那些事(1)

看了网上很多ffmpeg的编解码的文章和代码&#xff0c;发现有很多文章和代码都过时了&#xff0c;主要还是ffmpeg有很多接口都已经发生变化了。 这里简单说一下&#xff0c;什么是编码和解码。 1.视频编码 对于视频来说&#xff0c;可以理解为多张&#xff08;rgb或者yuv&…

Python散点图矩阵代码模版

本文分享Python seaborn实现散点图矩阵代码模版&#xff0c;节选自&#x1f449;嫌Matplotlib繁琐&#xff1f;试试Seaborn&#xff01; 散点图矩阵&#xff08;scatterplot matrix&#xff09;展示原始数据中所有变量两两之间关系&#xff0c;可以规避单一统计指标的偏差&…

Neural Filters:照片恢复

Ps菜单&#xff1a;滤镜/Neural Filters/恢复/照片恢复 Neural Filters/RESTORATION/Photo Restoration 照片恢复 Photo Restoration借助 AI 强大功能快速恢复旧照片&#xff0c;提高对比度、增强细节、消除划痕。将此滤镜与着色相结合以进一步增强效果。 “照片恢复”滤镜利用…

Scikit-Learn随机森林

Scikit-Learn随机森林 1、随机森林1.1、集成学习1.2、Bagging方法1.3、随机森林算法1.4、随机森林的优缺点2、Scikit-Learn随机森林回归2.1、Scikit-Learn随机森林回归API2.2、随机森林回归实践(加州房价预测)1、随机森林 随机森林是一种由决策树构成的集成算法,它在大多情况…

mac安装的VMware虚拟机进行桥接模式配置

1、先进行网络适配器选择&#xff0c;选择桥接模式 2、点击网络适配器 设置... 3、选择WiFi&#xff08;我使用的是WiFi&#xff0c;所以选择这个&#xff09;&#xff0c;注意看右边的信息&#xff1a;IP和子网掩码&#xff0c;后续配置虚拟机的ifcfg-ens文件会用到 4、编辑if…

【论文阅读笔记】The Google File System

1 简介 Google File System (GFS) 是一个可扩展的分布式文件系统&#xff0c;专为快速增长的Google数据处理需求而设计。这篇论文发表于2003年&#xff0c;此前已在Google内部大规模应用。 GFS不仅追求性能、可伸缩性、可靠性和可用性等传统分布式文件系统的设计目标&#xf…