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++;
                }
                // 每次执行完毕 -1
                latch.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解锁释放锁,作用于主内存的变量,允许其他线程获取该变量的锁并进行操作

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

多模态大模型:系统、趋势与问题

引言 多模态大模型是当今人工智能领域的热门方向之一。它不仅能处理文本&#xff0c;还能理解和生成图像、视频、语音等多种模态的数据。这种能力使得多模态大模型在自然语言处理、计算机视觉等多个领域展示出巨大的潜力和应用价值。那么&#xff0c;多模态大模型是如何训练出…

web自动化-数据驱动与失败用例截图、失败重新运行

因为只有失败的用例需要截图&#xff0c;那么问题就是&#xff1a; 什么时候用例会失败&#xff1f; 数据驱动测试 我们前面覆盖到的用例都是正常的用例&#xff0c;如果要测试异常的用例呢&#xff1f; 我们来写一下登录的异常 场景&#xff1a;【login_page】 # 用户输入框…

vivado设置Vscode为默认编辑器

D:\vscode\Microsoft VS Code\Code.exe -g [file name]:[line number]

开源大模型与闭源大模型:谁将引领AI的未来?

前言 在AI领域&#xff0c;开源大模型和闭源大模型一直并存&#xff0c;各自有其独特的优势和挑战。下面&#xff0c;我们将从数据隐私、商业应用和社区参与三个方向&#xff0c;对这两种模型进行深入探讨。 一、数据隐私 开源大模型&#xff1a; 1. 透明度高&#xff1a; …

YoloV8实战:各种图绘制汇总(mAP50、mAP50-95、loss、PR_curve、F1_curve)|科研必备|绘图神器

摘要 本文的内容是告诉大家如何绘制mAP50、mAP50-95、loss、PR_curve、F1_curve等图像,方便大家写论文。 绘制mAP50、mAP50-95、loss等图。 先上效果,如下图: 首先将,训练的result.csv汇总到一个文件夹下面(这样方便寻找),要不然找起来太麻烦。如下图: 我都放到re…

The Sandbox 和 Bitkub 联手增强东南亚元宇宙中心

作为去中心化游戏虚拟世界和区块链平台的先驱&#xff0c;The Sandbox 正与泰国领先的区块链网络 Bitkub Blockchain Technology Co., Ltd. 展开创新合作。双方合作的目的是将Bitkub元宇宙的影响力扩展到The Sandbox&#xff0c;建立一个元宇宙中心&#xff0c;向用户承诺从 Bi…

5.28学习总结

java复习总结 hashcode()和equals() hashcode():在Object里这个方法是通过返回地址的整数值来生成哈希值。 equals():在Object里这个方法是通过比较他们的内存地址来确定两个对象是否相同。 运行效率&#xff1a;hashcode的时间复杂度为O(1)&#xff08;因为只要计算一次哈…

搜维尔科技:【系统集成案例】三面CAVE系统案例

用户名称&#xff1a;成都东软学院 主要产品&#xff1a;工业激光投影机、光学跟踪系统、主动立体眼镜、主动式立体眼镜发生器 在4米x9米的空间内&#xff0c;通过三通道立体成像&#xff0c;对立体模型进行数字化验证&#xff0c;辅助unity课程设计。 立体投影大屏方案采用的…

颈源性头痛症状及表

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

Aleth-NeRF: Illumination Adaptive NeRF with Concealing Field Assumption

Abstract Aleth-NeRF: 带有隐蔽场假设的照明自适应 NeRF 照明照明标准的神经辐射场(NeRF)范例采用了一种以观察者为中心的方法,将光照和材料反射的各个方面仅仅从3D 点发射纠缠在一起。这种简化的渲染方法在准确建模在不利光照条件下捕获的图像方面提出了挑战,如弱光或过度曝…

MFC 发起 HTTP Post 请求 发送MES消息

文章目录 获取Token将获取的Token写入JSON文件 将测试参数发送到http首先将测试参数写入到TestData.JSON文件rapidjson 库需要将CString 进行类型转换才能使用&#xff0c;将CString 转换为const char* 发送JSON 参数到http中&#xff0c;并且获取返回结果写入TestFinish.JSON文…

vue3 使用css实现一个弧形选中角标样式

文章目录 1. 实现效果2. 实现demo 在前端开发中&#xff0c;ui同学经常会设计这样的样式&#xff0c;用于区分选中的状态 下面抽空简单些了一下&#xff0c;记录下&#xff0c;后面直接复制用 1. 实现效果 实现一个菜单切换&#xff0c;右下角有个角标的样式 2. 实现demo 主要…

【Qt QML】Dialog组件

带有标准按钮和标题的弹出对话框&#xff0c;用于与用户进行短期交互。 这个描述指的是一个常见的用户界面元素&#xff0c;即一个临时弹出的窗口&#xff08;或对话框&#xff09;&#xff0c;它包含一个标题&#xff0c;显示对话框的用途或内容描述&#xff0c;以及一系列标…

学习笔记——动态路由协议——OSPF(OSPF区域)

四、OSPF区域 OSPF路由器在同一个区域(Area)内网络中泛红LSA(链路状态通告)。为了确保每台路由器都拥有对网络拓扑的一致认知&#xff0c;LSDB需要在区域内进行同步。如果OSPF域仅有一个区域&#xff0c;随着网络规模越来越大&#xff0c;LSDB越来越庞大&#xff0c;OSPF路由器…

走进智慧仓储:3D可视化工厂园区革新物流新纪元

在快节奏的现代生活中&#xff0c;物流仓储行业扮演着至关重要的角色。随着科技的飞速发展&#xff0c;传统仓储模式正面临一场前所未有的变革。今天&#xff0c;就让我们一起看看3D可视化技术如何为物流行业带来前所未有的便利与效率。 什么是3D可视化工厂园区&#xff1f; 3…

flowable6springboot2 工作流从入门到精通

相关文档 https://tkjohn.github.io/flowable-userguide/ 文档手册 https://github.com/flowable/flowable-engine/releases/tag/flowable-6.8.0 flowable-ui下载地址 https://dlcdn.apache.org/tomcat/tomcat-8/v8.5.100/bin/apache-tomcat-8.5.100.zip tomcat下载 百度网盘…

效率工作:一键为多种资产添加统一材质(小插件)

1.需求分析&#xff1a; 当导入一批资产&#xff0c;或者有同一批结构体需要添加相同材质时&#xff0c;单独为每个模型都添加材质费时费力&#xff0c;有没有什么办法&#xff0c;能同时为多个资产添加材质。 2.操作实现 1.在网上找到了一款插件&#xff0c;经过验证&#xf…

SQL2017附加从其他电脑复制过来的mdf数据后出现【只读】无法写入数据

1. 尝试给它所在的文件夹的属性中的“只读”去勾&#xff0c;无果。 2. 其他文章提示是文件的问题。 该错误为文件权限错误&#xff0c;找到该数据库的 数据库文件 和 日志文件&#xff0c;在安全中添加 Authenticated Users 用户的权限&#xff0c;并设置 “完全控制”

Idea工具的使用技巧与常见问题解决方案

一、使用技巧 1、启动微服务配置 如上图&#xff0c;在编辑配置选项&#xff0c;将对应的启动入口类加进去&#xff0c; 增加jvm启动参数&#xff0c; 比如&#xff1a; -Denvuat 或者 -Denvuat -Dfile.encodingUTF-8 启动配置可能不是-Denvuat&#xff0c;这个自己看代…

04 FreeRTOS 队列(queue)

1、队列的特性 队列可以理解为一个传送带&#xff0c;一个流水线。 队列可以包含若干个数据&#xff1a;队列中有若干项&#xff0c;这被称为"长度"(length) 每个数据大小固定 创建队列时就要指定长度、数据大小 数据的操作采用先进先出的方法(FIFO&#xff0c;First…