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

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();
}

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

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

相关文章

力扣hot100 二叉树展开为链表 递归 特殊遍历

👨‍🏫 题目地址 👩‍🏫 参考题解 😋 将左子树插入到右子树上 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode() {}* …

基于ssm毕业设计选题系统论文

摘 要 现代经济快节奏发展以及不断完善升级的信息化技术,让传统数据信息的管理升级为软件存储,归纳,集中处理数据信息的管理方式。本毕业设计选题系统就是在这样的大环境下诞生,其可以帮助管理者在短时间内处理完毕庞大的数据信息…

ubuntu20快速搭建自己的git代码仓库环境

##安装docker groupadd docker apt install docker.io ##用户添加到docker组 sudo usermod -aG docker ${USER} ##用户添加sudo cat /etc/sudoers apt install vim ##sudoers文件权限可以写 chmod uw sudoers vim sudoers ##在root底下添加这行,yym改到自…

Android 项目适配64位架构后,腾讯X5内核加载失败解决方案

前些天发现了一个蛮有意思的人工智能学习网站,8个字形容一下"通俗易懂,风趣幽默",感觉非常有意思,忍不住分享一下给大家。 👉点击跳转到教程 在接入最新腾讯X5内核后,发现初始化会失败,在APP模块的build.gra…

关于设计模式的一点总结

一、GoF 23种设计模式 1.分类 GoF 23种设计模式可分为几类:创建型、结构型和行为型。如下表 分类设计模式创建型单例模式、工厂方法模式、抽象工厂模式、原型模式、建造者模式结构型代理模式、适配器模式、装饰者模式、桥接模式、组合模式、门面模式、享元模式行…

js——json对象相互转化——js基础积累

js——json对象相互转化——js基础积累 需求场景解决步骤1:定义一个变量接收此字段,方便处理解决步骤2: { 外面的双引号要去掉解决步骤3:使用正则去除参数中的\\解决步骤4:如果此参数必须以{开头,以}结尾解…

[C#]C# OpenVINO部署yolov8目标检测模型

【官方框架地址】 https://github.com/ultralytics/ultralytics.git 【算法介绍】 YOLOv8 抛弃了前几代模型的 Anchor-Base。 YOLO 是一种基于图像全局信息进行预测的目标检测系统。自 2015 年 Joseph Redmon、Ali Farhadi 等人提出初代模型以来,领域内的研究者们…

Transformer简略了解

Transformer出自论文:《Attention Is All You Need》 该论文的提出,对RNN循环神经网络产生了冲击,席卷了自然语言处理(NLP)领域,后续的GPT4.0版本也是根据其进行训练优化的 一、Transformer主体架构 可以简化分为Encoders和Decod…

绿色能源、引领未来-2024武汉国际氢能源及燃料电池产业展览会

绿色能源、引领未来-2024武汉国际氢能源及燃料电池产业展览会 2024武汉国际氢能源及燃料电池产业博览会 2024 Wuhan International Hydrogen Energy and Fuel Cell Industry Expo 同期举办:2024世界汽车制造技术暨智能装备博览会 时间:2024.8.14-16 …

Vue: 多个el-select不能重复选择相同属性

一、场景 1.需求&#xff1a; 用户可自由选择需要修改的对象并同时修改多个属性&#xff0c;需要校验修改对象不能重复选择&#xff0c;但是可供修改属性是固定的 2.目标效果&#xff1a; 二、实现 1.主要代码&#xff1a; <template><el-selectv-model"se…

uniapp中组件库丰富的Switch 开关选择器使用方法

目录 #平台差异说明 #基础使用 #加载中 #禁用switch #自定义尺寸 #自定义颜色 #自定义样式 #异步控制 API #Switch Props #Switch Event 选择开关用于在打开和关闭状态之间进行切换。 #平台差异说明 App&#xff08;vue&#xff09;App&#xff08;nvue&#xff0…

全国计算机等级考试| 二级Python | 真题及解析(10)

一、选择题 1.要实现将实数型变量a的值保留三位小数,以下python可以实现的是( ) A.a%0.001 B.a//0.001 C.round(a,3) D.round(3,a) 2.在Python中要交换变量a和b中的值,应使用的语句组是( )。 A…

[足式机器人]Part2 Dr. CAN学习笔记-Ch01自动控制原理

本文仅供学习使用 本文参考&#xff1a; B站&#xff1a;DR_CAN Dr. CAN学习笔记-Ch01自动控制原理 1. 开环系统与闭环系统Open/Closed Loop System1.1 EG1: 烧水与控温水壶1.2 EG2: 蓄水与最终水位1.3 闭环控制系统 2. 稳定性分析Stability2.1 序言2.2 稳定的分类2.3 稳定的对…

ssm基于echarts的基金交易网站的设计与实现论文

摘 要 计算机网络发展到现在已经好几十年了&#xff0c;在理论上面已经有了很丰富的基础&#xff0c;并且在现实生活中也到处都在使用&#xff0c;可以说&#xff0c;经过几十年的发展&#xff0c;互联网技术已经把地域信息的隔阂给消除了&#xff0c;让整个世界都可以即时通话…

Dockerfile + harbor详解

Dockerfileharbor私服 一 docker工作流 1. docker管理流程 2. 镜像仓库阿里 (1) 阿里私有仓库 公司内部管理项目涉及到的所有docker镜像&#xff0c;会使用私有仓库的方式&#xff0c;集中管理。 (2) 创建阿里Docker仓库 登录阿里云创建私有仓库 网址&#xff1a;容器镜像服…

Shell脚本学习笔记

1. 写在前面 工作中&#xff0c;需要用到写一些shell脚本去完成一些简单的重复性工作&#xff0c; 于是就想系统的学习下shell脚本的相关知识&#xff0c; 本篇文章是学习shell脚本整理的学习笔记&#xff0c;内容参考主要来自C语言中文网&#xff0c; 学习过程中&#xff0c;…

MySQL 存储引擎和索引类型介绍

1. 引言 MySQL 是一个流行的关系型数据库管理系统&#xff0c;提供多种存储引擎以满足不同的业务需求。本文将介绍几种常见的 MySQL 存储引擎和索引类型比较&#xff0c;并给出相应的示例。 2. 存储引擎概述 2.1 InnoDB 存储引擎 InnoDB 是 MySQL 的默认存储引擎&#xff0…

基于spring boot物流管理系统设计与实现

&#x1f345;点赞收藏关注 → 私信领取本源代码、数据库&#x1f345; 本人在Java毕业设计领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目希望你能有所收获&#xff0c;少走一些弯路。&#x1f345;关注我不迷路&#x1f345;一 、设计说明 1.1 课题背景及意…

FreeRTOS——互斥信号量知识总结及其实战

1互斥信号量的概念 1&#xff09;互斥信号量&#xff1a;是一个拥有优先级继承的二值信号量&#xff0c;在同步的应用中二值信号量最适合。互斥信号量适合用于那些需要互斥访问的应用中&#xff01; 2&#xff09;优先级继承&#xff1a;当一个互斥信号量正在被一个低优先级的…

LeetCode 每日一题 Day 32 ||递归单调栈

2487. 从链表中移除节点 给你一个链表的头节点 head 。 移除每个右侧有一个更大数值的节点。 返回修改后链表的头节点 head 。 示例 1&#xff1a; 输入&#xff1a;head [5,2,13,3,8] 输出&#xff1a;[13,8] 解释&#xff1a;需要移除的节点是 5 &#xff0c;2 和 3 。…