volatile 保证内存变量可见性的实现原理解析

news2024/12/28 5:31:21

目录

volatile 的定义        

可见性问题

JMM(JavaMemoryModel)

保证可见性

现代计算机的内存模型

MESI(缓存一致性协议)

嗅探

总线风暴

volatile 的两条实现原则


volatile 的定义        

        Java代码在编译后会编程 Java 字节码,字节码在被类加载器加载到 JVM 里,JVM 执行字节码,最终需要转化为汇编指令在 CPU 上执行,Java 所使用的并发机制依赖于 JVM 的实现和 CPU 指令。

        多线程中 synchronized 和 volatile 都扮演着重要的角色,volatile 是轻量级的 synchronized,它在多处理器开发中保证了共享变量的"可见性"(当一个线程修改了一个共享变量时,其他的线程能读到这个修改后的值)。如果 volatile 变量修饰符使用恰当的话,它比 synchronized 的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。

Java 语言规范第 3 版中对 volatile 的定义如下:Java 编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。

 

可见性问题

public class JMMDemo {
   
    private static int num = 0;

    public static void main(String[] args) { // main

        new Thread(()->{ // 线程 1 
            while (num==0){
            }
        }).start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        num = 1;
        System.out.println(num);
    }
}

上述代码在执行时可能出现死循环,原因在于线程 1 对主内存的变化不知道的,

该问题出现的原因在于线程1 获取不到 main 线程修改后的 num 值,为什么会出现这种问题?这里我们就要了解一下 Java 内存模型(简称 JMM)

JMM(JavaMemoryModel)

JMM:Java内存模型,是java虚拟机规范中所定义的一种内存模型,Java内存模型是标准化的,屏蔽掉了底层不同计算机的区别(注意这个跟JVM完全不是一个东西,只有还有小伙伴搞错的)。描述了Java程序中各种变量(线程共享变量)的访问规则,以及在JVM中将变量,存储到内存和从内存中读取变量这样的底层细节。

JMM有以下规定:

所有的共享变量都存储于主内存,这里所说的变量指的是实例变量和类变量,不包含局部变量,因为局部变量是线程私有的,因此不存在竞争问题。

每一个线程还存在自己的工作内存,线程的工作内存,保留了被线程使用的变量的工作副本。

线程对变量的所有的操作(读,取)都必须在工作内存中完成,而不能直接读写主内存中的变量。

不同线程之间也不能直接访问对方工作内存中的变量,线程间变量的值的传递需要通过主内存中转来完成。

本地内存和主内存的关系

正是因为这样的机制,才导致了可见性问题的存在

保证可见性

在 X86 处理器下通过工具获取 JIT 编译器 生成的汇编指令来查看对 volatile 进行写操作时,CPU 会做什么事情

instance = new Singleton();   // instance 是 volatile 变量

转换成汇编代码

0x01a3de1d: movb $0×0,0×1104800(%esi);
0x01a3de24: lock addl $0×0,(%esp);

有 volatile 变量修饰的共享变量进行写操作的时候会多出第二行汇编代码,通过查 IA-32 架构软件开发者手册可知,Lock 前缀的指令在多核处理器下会引发了两件事情 。

  • 将当前处理器缓存行的数据写回到系统内存。

  • 这个写回内存的操作会使在其他 CPU 里缓存了该内存地址的数据无效

为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2 或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了 volatile 的变量进行写操作,JVM 就会向处理器发送一条 Lock 前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里

现代计算机的内存模型

        其实早期计算机中cpu和内存的速度是差不多的,但在现代计算机中,cpu的指令速度远超内存的存取速度,由于计算机的存储设备与处理器的运算速度有几个数量级的差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲。

将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。

基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是也为计算机系统带来更高的复杂度,因为它引入了一个新的问题:缓存一致性(CacheCoherence)

在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(MainMemory)。

 

MESI(缓存一致性协议)

        当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

问题:既然现代处理器都实现了 MESI,为什么 Java还要从软件层面提供 volatile 关键字?

既然CPU有缓存一致性协议(MESI),为什么JMM还需要volatile关键字? - 知乎

 怎么发现数据是否失效呢?

嗅探

        每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

总线风暴

由于Volatile的MESI缓存一致性协议,需要不断的从主内存嗅探和cas不断循环,无效交互会导致总线带宽达到峰值。

所以不要大量使用Volatile,至于什么时候去使用Volatile什么时候使用锁,根据场景区分。

volatile 的两条实现原则

Lock 前缀指令会引起处理器缓存回写到内存

        Lock 前缀指令导致在执行指令期间,声言处理器的 LOCK#信号。在多处理器环境中,LOCK#信号确保在声言该信号期间,处理器可以独占任何共享内存。但是,在最近的处理器里,LOCK#信号一般不锁总线,而是锁缓存,毕竟锁总线开销的比较大。对于 Intel486 和 Pentium 处理器, 在锁操作时,总是在总线上声言 LOCK#信号。但在 P6 和目前的处理器中,如果访问的内存区域已经缓存在处理器内部,则不会声言 LOCK#信号。相反,它会锁定这块内存区域的缓存并回写到内存,并使用缓存一致性机制来确保修改的原子性,此操作被称为“缓 存锁定”,缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据

一个处理器的缓存回写到内存会导致其他处理器的缓存无效

        IA-32 处理器和 Intel 64 处理器使用 MESI(修改、独占、共享、无效)控制协议去维护内部缓存和其他处理器缓存的一致性。在多核处理器系统中进行操作的时候,IA-32 和 Intel 64 处理器能嗅探其他处理器访问系统内存和它们的内部缓存。处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。例如, 在 Pentium 和 P6 family 处理器中,如果通过嗅探一个处理器来检测其他处理器打算写内存地址,而这个地址当前处于共享状态,那么正在嗅探的处理器将使它的缓存行无效, 在下次访问相同内存地址时,强制执行缓存行填充

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

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

相关文章

GD(兆易创新)系列FLASH进行FPGA和ZYNQ配置固化相操作

写在前面 本文主要针对使用GD(兆易创新)系列的FLASH做启动配置片时,遇到的相关问题进行简单整理复盘,避免后人踩坑。 本人操作固化芯片型号为:ZYNQ7045、690T(复旦微替代型号V7 690T)。 7系列…

02-waf绕过漏洞发现之代理池指纹被动探针

WAF绕过-漏洞发现之代理池指纹被动探针 思维导图 漏洞发现触发WAF点-针对xray工具,awvs工具等 1.扫描速度(绕过方法:代理池,延迟,爬虫白名单)2.工具指纹(绕过方法:特征指纹&#x…

Qt Quick - Container

Qt Quick - Container使用总结 一、概述二、使用容器三、管理当前索引四、容器实现 一、概述 Container 提供容器通用功能的抽象基类。Container是类容器用户界面控件的基本类型,允许动态插入和删除Item。DialogButtonBox, MenuBar, SwipeView, 和 TabBar 都是继承…

测试工程师为什么要关注研发效能?

研发效能中的“研发”,指的是广义的研发团队,包含开发、测试、和研发团队内部的产品经理(不包含业务部门的产品经理)。测试工程师身处其中,作为研发团队的一员,对于整体的效能如何提升也应该了然于胸。这篇…

【论文写作】如何写科技论文?万能模板!!!(以IEEE会议论文为例)

0. 写在前面 常言道,科技论文犹如“八股文”,有固定的写作模式。本篇博客主要是针对工程方面的论文的结构以及写作链条的一些整理,并不是为了提高或者润色一篇论文的表达。基本上所有的论文,都需要先构思好一些点子,有…

「计算机控制系统」5. 模拟设计法

模拟控制器的离散化 数字PID控制器 Smith预估控制 文章目录 模拟控制器的离散化数值积分法一阶后向差分法一阶前向差分法双线性变换法(Tustin) 零极点匹配法其他方法 数字PID控制器模拟PID控制器的离散化数字PID的改进PID控制各环节的作用PID参数的整定扩…

win11删除的文件不在回收站原因及找回文件方法

win11是微软最新推出的操作系统,它的外观和功能都有所升级。但是,在使用win11的过程中,有时候你会误删一些重要的文件,而这些文件并没有进入回收站,这该怎么办呢?win11删除的文件不在回收站怎么找回&#x…

[强化学习]学习路线和关键词拾零

强化学习学习方法和路线 学习路线 先从基础教材开始,构建RL的知识框架,熟悉关键名词和公式推导,扩展到Model-Free的Value-Based和Policy-Based方法,同时参考github的代码练习。接下来精读几篇经典论文,如DQN,PPO等。…

Node内置模块 【压缩zlib模块】

文章目录 🌟前言🌟zlib模块🌟关于gzip与deflate🌟使用zlib🌟压缩与解压缩🌟案例:压缩🌟案例:解压缩 🌟服务端gzip压缩🌟HTTP配置🌟HTT…

Android Binder图文详解和驱动源码分析

文章目录 前言一、跨进程通讯的过程1. AIDL客户端代码2. AIDL服务端代码3. 通信过程a. 发送请求时序图b. 接收请求时序图 二、Binder一次拷贝1. 发送给Binder驱动的数据2. 一次拷贝示意图 三、Binder驱动源码1. 相关数据结构2. 阅读Binder驱动源码 参考 前言 最近在学习Binder…

Jupyter Notebook的安装与使用

Jupyter Notebook Jupyter Notebook介绍Jupyter Notebook使用安装启动创建文件编写代码和文本常用命令配置文件 Anaconda Jupyter Notebook介绍 Jupyter Notebook是一个基于Web的交互式计算环境,可以让用户以文档形式记录代码、数据分析结果和说明文本,并…

认识ThinkPHP框架

认识ThinkPHP框架 前言一、MVC框架体系二、 ThinkPHP框架文件夹结构三. ThinkPHP下载和基本配置四. ThinkPHP其他东西 前言 ThinkPHP框架是一款非常优秀的PHP框架,是完全由中国人发明的框架 一、MVC框架体系 ThinkPHP框架由MVC框架体系构成,MVC的解释如下…

ubuntu下安装配置grpc

目录 1.准备环境 2.安装protobuf 3.安装cares库 3.安装grpc-1.17.x 1.准备环境 sudo apt-get install pkg-config sudo apt-get install autoconf automake libtool make g unzip sudo apt-get install libgflags-dev libgtest-dev sudo apt-get install clang libc-dev 如…

linux中的vim编辑器

Vim是一款强大的文本编辑器,可以在终端中使用。它有很多优点,比如快速、高效、灵活等,但同时也有一些难以掌握的操作。在本篇博客中,我们将详细介绍Vim的各种功能,以及如何使用它来提高的编辑效率。 1.基本模式 Vim具…

Unity之ShaderGraph入门

前言 随着Unity版本的不断升级,URP(可编程渲染管线)也越来越普及了。不管是从效果还是性能,都是吊打老版的build-in-shader。所以无论如何我们都要开始 拥抱URP,升级Unity的时候到了。 引擎版本 我这里选择了Unity …

01_Linux操作系统

第一章:Linux操作系统 阶段内容说明: Linux命令:软件测试第一个任务,一般都要进行环境搭建,一部分环境搭建内容是在服务器上实现的,跟服务器交互需要使用Linux命令(因为服务器没有图形化界面&a…

Atlassian Confluence CVE-2022-26134 RCE漏洞

Atlassian Confluence CVE-2022-26134 RCE漏洞 Atlassian Confluence CVE-2022-26134 RCE漏洞 漏洞简介 远程攻击者在未经身份验证的情况下,可构造OGNL表达式进行注入,实现在Confluence Server或Data Center上执行任意代码. 漏洞影响范围 Confluence …

代码优化- 基本概念

思考一个问题:我们可以再抽象语法树上做编译优化吗? 答案是否定的,如果在抽象语法树上做编译优化的话,程序员所写的可能包含错误的代码,可能就被删除了,比如,对下面的程序做不可达代码删除优化…

Hadoop笔记整理

Hadoop 一. 引言 1.1 什么是大数据 大数据:(Big Data):数据量级很大的应用处理。TB级 ,日数据增长GB级 K -- M---- G ---- T ----PB ---- EB ---ZB 1024通过对海量数据进行分析,挖掘,进而发现数据内在的规律,从而为企业或者…

【数据结构】超详细讲解:算术表达式转化为后缀表达式、前缀表达式、表达式树的构建

作者:努力学习的大一在校计算机专业学生,热爱学习和创作。目前在学习和分享:算法、数据结构、Java等相关知识。博主主页: 是瑶瑶子啦所属专栏: 【数据结构】:该专栏专注于数据结构知识,持续更新&#xff0c…