【JUC】Volatile关键字+CPU/JVM底层原理

news2024/9/21 18:46:09

Volatile关键字

volatile内存语义

1.当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。
2.当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取。

volatile两大特点

可见性:是指当一个线程修改了某一个共享变量的值,其他线程是能够立即知道该变更 ,JMM规定了所有的变量都存储在主内存中。

Java中普通的共享变量不保证可见性,因为数据修改被写入内存的时机是不确定的,多线程并发下很可能出现"脏读",所以每个线程都有自己的工作内存,线程自己的工作内存中保存了该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等 )都必需在线程自己的工作内存中进行,而不能够直接读写主内存中的变量。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成

有序性:对于一个线程的执行代码而言,我们总是习惯性认为代码的执行总是从上到下,有序执行。但为了提供性能,编译器和处理器通常会对指令序列进行重新排序。指令重排可以保证串行语义一致,但没有义务保证多线程间的语义也一致,即可能产生"脏读"

简单说,两行以上不相干的代码在执行的时候有可能先执行的不是第一条,不见得是从上到下顺序执行,执行顺序会被优化。

注意:volatile修饰的变量复合操作不具有原子性

volatile底层原理:内存屏障

什么是内存屏障

内存屏障(Memory Barriers / Memory Fences)(也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。内存屏障其实就是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令,volatile实现了Java内存模型中的可见性和有序性,但volatile无法保证原子性。

内存屏障之前的所有写操作都要回写到主内存,
内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)。

内存屏障基于计算机指令实现

内存屏障的作用

1.阻止屏障两边的指令重排序

2.写数据时加入屏障,强制将线程私有工作内存的数据刷回到主物理内存

3.读数据时加入屏障,线程私有工作内存的数据失效,重新到著物理内存中获取最新数据

JVM中四类内存屏障指令

屏障类型指令示例说明
LoadLoadLoad1; LoadLoad ; Load2保证Load1的读取操作在Load2以及后续操作之前
StoreStoreStrore1; StoreStore; Store2在Store2及其后续写操作执行前,保证Store1的写操作结果刷新到主内存
LoadStoreLoad1; LoadStore; Store1在Store1及其后的写操作执行前,保证Load1的读操作已经结束
StoreLoadStore1; StoreLoad; Load1保证Store1的写操作结果已刷新到主内存之后,Load1及其后的读操作才开始

happen-before 之volatile变量规则

第一个操作第二个操作:普通读写第二个操作:volatile读第二个操作:volatile写
普通读写×
volatile读×××
volatile写××

当第一个操作为volatile读时,不论第二个操作是什么,都不能重排序。这个操作保证了volatile读之后的操作不会被重排到volatile读之前。
当第二个操作为volatile写时,不论第一个操作是什么,都不能重排序。这个操作保证了volatile写之前的操作不会被重排到volatile写之后。
当第一个操作为volatile写时,第二个操作为volatile读时,不能重排。

JMM内存屏障插入策略

写 : 1. 在每个 volatile 写操作的前⾯插⼊⼀个 StoreStore 屏障
2.在每个在每个 volatile 写操作的后⾯插⼊⼀个 StoreLoad 屏障

store -> (store写)->laod

读: 1. 在每个 volatile 读操作的后⾯插⼊⼀个 LoadLoad 屏障
2.在每个 volatile 读操作的后⾯插⼊⼀个 LoadStore 屏障

load->(load读)->store

在这里插入图片描述

volitile关键字会在字节码添加flags :ACC_VOLATILE

JVM再把字节码转化为机器码的时候,会根据JMM规范为其相应位置插入内存屏障指令

CPU底层volatile

cpu执行机器码指令的时候,是使用lock前缀命令来实现volatile的功能的

lock指令相当于内存屏障,功能也类似于内存屏障:

(1)首先对总线/缓存加锁,然后去执行后面的命令,最后释放锁,同时把高速缓存的数据刷新到主内存

(2)在lock锁住总线/缓存的时候,其他cpu的读写请求就会被阻塞,直到锁释放。lock过后的写操作会让其他cpu中高速缓存的相应的数据失效,这样后续这些cpu在读取数据的时候就会从主存中加载最新的数据

volitile使用场景

1.volatile修饰的变量单一赋值可以,但是复合运算赋值不可以(i++), 因为i++字节码中被拆分为三个指令:getfield :执行拿到原始i iadd:加一操作 putfield:累加后的值写回

在这里插入图片描述

2.状态标志,判断业务是否结束

public class Demo
{
   //  * 使用:作为一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如完成初始化或任务结束
 //* 理由:状态标志并不依赖于程序内任何其他状态,且通常只有一种状态转换
 //* 例子:判断业务是否结束

    private volatile static boolean flag = true;

    public static void main(String[] args)
    {
        new Thread(() -> {
            while(flag) {
                //do something......
            }
        },"t1").start();

        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(2L); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            flag = false;
        },"t2").start();
    }
}
 

3.开销较低的读,写锁策略

public class Demo
{
    /**
     * 使用:当读远多于写,结合使用内部锁和 volatile 变量来减少同步的开销
     * 理由:利用volatile保证读取操作的可见性;利用synchronized保证复合操作的原子性
     */
    public class Counter
    {
        private volatile int value;

        public int getValue()
        {
            return value;   //利用volatile保证读取操作的可见性
              }
        public synchronized int increment()
        {
            return value++; //利用synchronized保证复合操作的原子性
               }
    }
}

4.dcl双重检查锁

public class SafeDoubleCheckSingleton
{
    private static SafeDoubleCheckSingleton singleton;
    //私有化构造方法
    private SafeDoubleCheckSingleton(){
    }
    //双重锁设计
    public static SafeDoubleCheckSingleton getInstance(){
        if (singleton == null){
            //1.多线程并发创建对象时,会通过加锁保证只有一个线程能创建对象
            synchronized (SafeDoubleCheckSingleton.class){
                if (singleton == null){
                    //隐患:多线程环境下,由于重排序,该对象可能还未完成初始化就被其他线程读取
                    singleton = new SafeDoubleCheckSingleton();
                }
            }
        }
        //2.对象创建完毕,执行getInstance()将不需要获取锁,直接返回创建对象
        return singleton;
    }
}
 

单线程环境下 singleton = new SafeDoubleCheckSingleton();回进行如下操作;
在这里插入图片描述

但是多线程环境下,在"问题代码处",会执行如下操作,由于重排序导致2,3乱序,后果就是其他线程得到的是null而不是完成初始化的对象

在这里插入图片描述

解决方法:

1.volatile修饰

public class SafeDoubleCheckSingleton
{
    //通过volatile声明,实现线程安全的延迟初始化。
    private volatile static SafeDoubleCheckSingleton singleton;
    //私有化构造方法
    private SafeDoubleCheckSingleton(){
    }
    //双重锁设计
    public static SafeDoubleCheckSingleton getInstance(){
        if (singleton == null){
            //1.多线程并发创建对象时,会通过加锁保证只有一个线程能创建对象
            synchronized (SafeDoubleCheckSingleton.class){
                if (singleton == null){
                    //隐患:多线程环境下,由于重排序,该对象可能还未完成初始化就被其他线程读取
                                      //原理:利用volatile,禁止 "初始化对象"(2) 和 "设置singleton指向内存空间"(3) 的重排序
                    singleton = new SafeDoubleCheckSingleton();
                }
            }
        }
        //2.对象创建完毕,执行getInstance()将不需要获取锁,直接返回创建对象
        return singleton;
    }
}

2.静态内部类

public class SingletonDemo
{
    private SingletonDemo() { }

    private static class SingletonDemoHandler
    {
        private static SingletonDemo instance = new SingletonDemo();
    }

    public static SingletonDemo getInstance()
    {
        return SingletonDemoHandler.instance;
    }
}

既然一修改就是可见,为什么还不能保证原子性?

在这里插入图片描述

volatile主要对其中的部分指令做了处理

在这里插入图片描述

要use(使用)一个变量的时候必需load(载入),要载入的时候必需从主内存read(读取)这样就解决了读的可见性。

写操作是把assign和store做了关联(在assign(赋值)后必需store(存储))。store(存储)后write(写入)。
也就是做到了给一个变量赋值的时候一串关联指令直接把变量值写到主内存。

就这样通过用的时候直接从主内存取,在赋值到直接写回主内存做到了内存可见性。注意蓝色框框的间隙

多线程并发导致的问题有三个:

  1. CPU缓存引起的可见性问题
  2. CPU指令重排引起的有序性问题
  3. CPU轮换线程中断导致的原子性问题

volatile修饰词前两个可以杜绝,对于3无法:

S:线程1取i,进行1取出值,结果存入某个寄存器,W:线程切换到2执行1,2,3,内存刷新,H:线程再次切换到线程1,线程1执行3

这里线程1的缓存按理说应该是失效的,因为W操作以后i的值已经更新了,事实确实缓存已经失效了,但是寄存器里面已经存入值了,所以就直接使用了寄存器里面的值进行在AL寄存器+1操作,然后写入i的地址。相反如果寄存器里面没有值,这时cpu缓存也失效了,就必须先从主内存里面获取i的值,然后再导入寄存器。

CPU/JVM底层原理

volatile关键字的作用是:修饰的对象在进行写操作的完成时候会立即将变量的值从工作的线程空间刷新回主内存;在执行读操作前会从主内存中获取最新的值。这些功能是由JMM规定的内存屏障插入策略实现的。

CPU多核处理器之间缓存不一致现象是通过MESI协议实现的,但是MESI协议下cpu执行对变量执行操作后缓存行状态通信需要发送信息给其他缓存了该数据的CPU,并且要等到他们确认回执,这段时间CPU是阻塞状态的,因此CPU引入了store buffers,cpu直接将共享数据写入store bufferes同时发送消息,然后去处理其他指令.其他cpu发送反馈消息后再将store bufferes中的缓存存储到缓存行,最后同步到主内存,这种异步优化导致了CPU的对内存的乱序访问带来的可见性问题,因此CPU层面引入了内存屏障让软件层面决定禁止指令重排序,因此votalie底层是通过CPU的MESI协议和访存排序来保证可见性和有序性的,而可见性又是在有序性基础上保证的。

对于加强访存排序x86平台主要有以下几种手段:
ifence,sfence,mfence(序列化指令),io指令,加锁指令,序列化指令,lock前缀(指的是lock开头的一系列指令)等进行强排序。

jVM底层是使用了lock前缀实现的。

lock前缀会对CPU总线和缓存进行加锁,然后执行后面的命令,执行完命令后将脏数据从缓存立即刷新到主内存而不需要刷新到store bufferes,同时加锁期间其他cpu核心对总线和缓存的访问会被阻塞,释放锁后其他CPU核心相应的cache line会失效然后从主内存重新加载,这个是由MESI协议实现的,同时访存排序模型也规定了:读写操作都不能跨越加锁指令和序列化指令,因此保证了有序性和可见性。因此lock前缀同时达到了MESI和强指令排序的效果。

其中loadload和storeload,loadstore屏障在x86处理器上不需要指令,而是插入了一段特定的空的内联汇编块防止编译器重排序,CPU层面的防止重排序是由x86平台默认的访存排序实现的,而stroeload是在基础上加入了lock前缀来加强了访存排序实现了全内存屏障。

static inline void compiler_barrier() {
  __asm__ volatile ("" : : : "memory");
}

inline void OrderAccess::loadload()   { compiler_barrier(); }
inline void OrderAccess::storestore() { compiler_barrier(); }
inline void OrderAccess::loadstore()  { compiler_barrier(); }
inline void OrderAccess::storeload()  { fence();            }

inline void OrderAccess::acquire()    { compiler_barrier(); }
inline void OrderAccess::release()    { compiler_barrier(); }

inline void OrderAccess::fence() {
   // always use locked addl since mfence is sometimes expensive
#ifdef AMD64
  __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
  __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
  compiler_barrier();
}
  • compiler_barrier():这是一个编译器屏障,它使用了一个空的内联汇编块来阻止编译器进行指令重排序。这个函数没有任何运行时开销,但可以阻止编译器在优化过程中改变指令的顺序。
  • loadload(), storestore(), loadstore(), acquire(), release():这些函数都调用了compiler_barrier(),因此它们的效果与compiler_barrier()相同。
  • storeload():这个函数调用了fence(),它提供了一个全内存屏障。这意味着在fence()之前的所有内存访问(读取和写入)在fence()执行之前完成,而在fence()之后的所有内存访问在fence()执行之后开始。
  • fence():这个函数提供了一个全内存屏障。它使用了一个带有lock前缀的addl指令来阻止处理器进行指令重排序。lock前缀会锁定总线,确保指令的原子性。这个函数在执行完lock addl指令后又调用了compiler_barrier(),以阻止编译器进行指令重排序。

对于Linux_AMD_x86: storesload是lock;addl 0,(sp)指令,

always use locked addl since mfence is sometimes expensive

#ifdef AMD64
  __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
  __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
  compiler_barrier();
StoreLoad屏障:
    使用addl 0,(sp)指令来实现这是一种加法指令,它的作用是堆栈指针(sp)所指向的内存地址进行加0的操作,并且在执行这个操作的过程中,使用lock指令来修饰。这个指令的作用是保证在它之前的所有写操作都对其他处理器可见,然后才执行它之后的所有操作。这样就可以保证这个内存地址的值对所有的处理器是一致的,也就是实现了StoreLoad屏障的功能。

JVM的内存屏障是由lock addl 0,(sp)实现的,

volitile关键字会在字节码添加flags :ACC_VOLATILE

JVM再把字节码转化为机器码的时候,会根据JMM规范为其相应位置插入内存屏障指令

读操作前插入loadload,后插入loadstore

写操作前插入storestore,写操作后插入storeload

多线程并发导致的问题有三个:

  1. CPU缓存引起的可见性问题
  2. CPU指令重排引起的无序性问题
  3. CPU轮换线程中断导致的原子性问题

通过上面对volatile底层操作,volatile可以解决可见性和无序性问题,但是无法保证原子性问题。

JDK12源码:/src/hotpot/share/runtime/orderAccess.hpp

/src/hotpot/os_cpu/linux_x86/orderAcces_linux_x86.hpp

在这里插入图片描述

window_x64

#ifdef AMD64
  StubRoutines_fence();
#else
  __asm {
    lock add dword ptr [esp], 0;
  }
#endif // AMD64
  compiler_barrier();
}

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

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

相关文章

运输层

title: 运输层 date: 2023-12-24 14:17:55 tags: 知识总结 categories: 计算机网络 运输层和网络层的联系和区别 物理层、数据链路层以及网络层它们共同解决了将主机通过异构网络互联起来所面临的问题,实现了主机到主机的通信,但实际上,在计…

视频监控可视化云平台EasyCVR智能视频技术优势分析

TSINGSEE青犀视频安防视频管理系统EasyCVR视频智能融合共享平台,是一个支持Windows/Linux(CentOS ubuntu)/国产化系统的视频管理平台。平台可以支持多协议接入,通过视频应用引擎将多种格式的视频数据转换为统一的视频流数据,支持无插件H5直播…

RocketMQ单机部署完整学习笔记

文章目录 前言一、RocketMQ是什么?二、使用步骤1.安装MQ1.安装JDK2.安装mq3.MQ配置(核心) 2.搭建可视化dashboard1.下载源码2.修改配置3.启动 3.整合java1.生产者2.消费者3.启动生产者4.启动消费者5.dashboard添加消费组 三、总结全部的配置 前言 本文是基于4.X版本…

数据库攻防学习

免责声明 本文仅供学习和研究使用,请勿使用文中的技术用于非法用途,任何人造成的任何负面影响,与本号及作者无关。 Redis 0x01 redis学习 在渗透测试面试或者网络安全面试中可能会常问redis未授权等一些知识,那么什么是redis?redis就是个数据库&#xff…

【UEFI基础】EDK网络框架(通用函数和数据)

通用函数和数据 DPC DPC全称Deferred Procedure Call。Deferred的意思是“延迟”,这个DPC的作用就是注册函数,然后在之后的某个时刻调用,所以确实是有“延迟”的意思。DPC在UEFI的实现中包括两个部分。一部分是库函数DxeDpcLib,…

知识付费平台搭建?找明理信息科技,专业且高效

明理信息科技知识付费saas租户平台 在当今数字化时代,知识付费已经成为一种趋势,越来越多的人愿意为有价值的知识付费。然而,公共知识付费平台虽然内容丰富,但难以满足个人或企业个性化的需求和品牌打造。同时,开发和…

【MATLAB第88期】基于MATLAB的6种神经网络(ANN、FFNN、CFNN、RNN、GRNN、PNN)多分类预测模型对比含交叉验证

【MATLAB第88期】基于MATLAB的6种神经网络(ANN、FFNN、CFNN、RNN、GRNN、PNN)多分类预测模型对比含交叉验证 前言 本文介绍六种类型的神经网络分类预测模型 1.模型选择 前馈神经网络 (FFNN) 人工神经网络 (ANN) 级联前向神经网络 (CFNN) 循环神经网…

QT上位机开发(串口界面设计)

【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing 163.com】 如果上位机要和嵌入式设备进行打交道的话,那么串口可能就是我们遇到的第一个硬件设备。串口的物理接线很简单,基本上就是收…

前端-relation-graph实现关系数据展示(关系图/流程图)

目录 前言: 1. relation-graph 2. relation-graph数据关系组件---官方地址relation-graph - A Relationship Graph Componenthttps://www.relation-graph.com/ 3. 选择relation-graph的理由 4. 项目中引用relation-graph 4.1 下载命令 4.2 在Vue 2 中使用 4…

CGAL的无限制的Delaunay图

本章描述了构建L∞距离下线段Delaunay图的算法和几何特征。这些特征还包括绘制L∞距离下线段Delaunay图对偶(即L∞距离下线段Voronoi图)边缘的方法。L∞算法和特征依赖于欧几里得(或L2)距离下的线段Delaunay图算法和特征。L∞度量…

【动态规划】C++算法:44 通配符匹配

作者推荐 【动态规划】【字符串】扰乱字符串 本文涉及的基础知识点 动态规划 LeetCode44 通配符匹配 给你一个输入字符串 (s) 和一个字符模式 ,请你实现一个支持 ‘?’ 和 ‘’ 匹配规则的通配符匹配: ‘?’ 可以匹配任何单个字符。 ’ 可以匹配…

动手学深度学习之卷积神经网络之池化层

池化层 卷积层对位置太敏感了,可能一点点变化就会导致输出的变化,这时候就需要池化层了,池化层的主要作用就是缓解卷积层对位置的敏感性 二维最大池化 这里有一个窗口,来滑动,每次我们将窗口中最大的值给拿出来 还是上…

大创项目推荐 深度学习人脸表情识别算法 - opencv python 机器视觉

文章目录 0 前言1 技术介绍1.1 技术概括1.2 目前表情识别实现技术 2 实现效果3 深度学习表情识别实现过程3.1 网络架构3.2 数据3.3 实现流程3.4 部分实现代码 4 最后 0 前言 🔥 优质竞赛项目系列,今天要分享的是 🚩 深度学习人脸表情识别系…

【JavaFX】JavaFX11开发踩坑记录

文章目录 技术栈踩坑记录 技术栈 JavaFX 11MavenJDK 11 踩坑记录 这些坑对于初学者很容易踩,JavaFX经常会报错空指针异常遇到其中一个问题可能就会消耗好几天的时间。 JavaFX 采用的是MVC架构设计,页面设计使用 fxml文件;业务逻辑采用Con…

k8s的网络

k8s的网络 k8s中的通信模式: 1、pod内部之间容器与容器之间的通信 在同一个pod中的容器共享资源和网络,使用同一个网络命名空间,可以直接通信的 2、同一个node节点之内,不同pod之间的通信 每个pod都有一个全局的真实的ip地址…

qt 异常汇总

1. C2338 No Q_OBJECT in the class with the signal (编译源文件 ..\..\qt\labelme-master\src\mainwindow.cpp mainwindow头文件中的类没有Q_OBJECT宏定义,或者其子类或者其他依赖没有Q_OBJECT宏定义。 全部qt类都要写上Q_OBJECT. 2. C2385 对connect的访…

AI:116-基于深度学习的视频行为识别与分析

🚀点击这里跳转到本专栏,可查阅专栏顶置最新的指南宝典~ 🎉🎊🎉 你的技术旅程将在这里启航! 从基础到实践,深入学习。无论你是初学者还是经验丰富的老手,对于本专栏案例和项目实践都有参考学习意义。 ✨✨✨ 每一个案例都附带有在本地跑过的关键代码,详细讲解供…

ctfshow——PHP特性

文章目录 web 89web 90web 91web 92web 93web 94web 95web 96web 97web 98web 99 web 89 使用人工分配 ID 键的数值型数组绕过preg_match. 两个函数: preg_match():执行正则表达式,进行字符串过滤。preg_match函数用法,正则表达式…

uni-app 前后端调用实例 基于Springboot 详情页实现

锋哥原创的uni-app视频教程: 2023版uniapp从入门到上天视频教程(Java后端无废话版),火爆更新中..._哔哩哔哩_bilibili2023版uniapp从入门到上天视频教程(Java后端无废话版),火爆更新中...共计23条视频,包括:第1讲 uni…

【Java EE初阶七】多线程案例(生产者消费者模型)

1. 阻塞队列 队列是先进先出的一种数据结构; 阻塞队列,是基于队列,做了一些扩展,适用于多线程编程中; 阻塞队列特点如下: 1、是线程安全的 2、具有阻塞的特性 2.1、当队列满了时,就不能往队列里…