【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");
#endifcompiler_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");
#endifcompiler_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 AMD64StubRoutines_fence();
#else__asm {lock add dword ptr [esp], 0;}
#endif // AMD64compiler_barrier();
}

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

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

相关文章

ARTrack 阅读记录

目录 环境配置与脚本编写 前向传播过程 网络结构 环境配置与脚本编写 按照官网执行并没有顺利完成,将yaml文件中的 pip 项 手动安装的 conda create -n artrack python3.9 # 启动该环境,并跳转到项目主目录路径下 astor0.8.1 configparser5.2.0 data…

C++学习笔记——友元及重载运算符

目录 一、友元 1.1声明友元函数 1.2声明友元类 二、运算符重载 2.1重载加号运算符 2.2重载流插入运算符 三、一个简单的银行管理系统 四、 详细的介绍 一、友元 在 C 中,友元是一个函数或类,它可以访问另一个类的私有成员或保护成员。通常情况下…

uView Alert 提示

用于页面中展示重要的提示信息。 基础用法# Alert 组件不属于浮层元素,不会自动消失或关闭。 Alert 组件提供四种类型,由 type 属性指定,默认值为 info。 success alert info alert warning alert error alert 主题# Alert 组件提供了…

HTML小白入门基础(概述,结构与基本常用标签)

目录 一、什么是HTML 二、HTML的基本结构: 三、结构与属性: 四、常见标签: 一、什么是HTML HTML:超文本标记语言(HyperText Markup Language) 超文本:指的是网页中可以显示的内容(图片&#x…

【Python机器学习】基于随机森林全球经济危机预测

一、引言 全球经济危机是一个复杂的问题,受到多种因素的影响,如金融市场、政策环境、地缘政治等。预测经济危机对于政策制定者、投资者和企业来说至关重要,因为它可以帮助他们提前做出应对措施,减少潜在的损失。然而,准确预测经济危机是一项具有挑战性的任务,因为涉及到…

【LeetCode739】每日温度

1、题目描述 【题目链接】 给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。…

ROS+moveit+jakaminicob仿真运动

先浅浅的放一个官方的c文档: Motion Planning API — moveit_tutorials Melodic documentation 目录 一、实现运动到目标点的程序 二、在rviz里面新建扫描平台 一、实现运动到目标点的程序 (等我得空了补一个c运行环境部署说明) #inclu…

【Linux】CentOS 7重装保留数据的方法

我们需要重装CentOS 7系统,但是又想保留原来的数据。这篇文章将会从多个方面详细介绍如何重装CentOS 7系统,同时又能保留原有的数据。 一、备份重要数据 在重装CentOS 7系统之前,我们需要备份我们的重要数据。这可以通过多种方式实现&#…

React16源码: React中创建更新的方式及ReactDOM.render的源码实现

React当中创建更新的主要方式 ReactDOM.render || hydrate 这两个API都是我们要把整个应用第一次进行渲染到我们的页面上面能够展现出来我们整个应用的样子的一个过程这是初次渲染 setState 后续更新应用 forceUpdate 后续更新应用 replaceState 在后续被舍弃 关于 ReactDOM…

Qt undefined reference to `vtable for xxx‘

一、问题背景 在编译QT代码时,出现 undefined reference to xxx::entered(),通过鼠标双击QtCreator“问题栏”中的该行,则会跳转到发送信号的代码所在行。与上述代码一同出现在“问题栏”的还有 undefined reference to vtable for xxx’。 …

Git常用命令diff和mv

Git常用命令diff和mv 1、diff # 查看工作区和暂存区所有文件的对比 # 该命令可以显示尚未添加到stage的文件的变更 $ git diff# 查看工作区和暂存区单个文件的对比 $ git diff file# 显示暂存区和上一个commit的差异 # 查看暂存区与指定提交版本的不同,版本可缺省为HEAD $ gi…

力扣(leetcode)第412题Fizz Buzz(Python)

412.Fizz Buzz 题目链接:412.Fizz Buzz 给你一个整数 n ,找出从 1 到 n 各个整数的 Fizz Buzz 表示,并用字符串数组 answer(下标从 1 开始)返回结果,其中: answer[i] “FizzBuzz” 如果 i 同…

Linux-文件系统管理实验2

1、将bin目录下的所有文件列表放到bin.txt文档中,并将一共有多少个命令的结果信息保存到该文件的最后一行。统计出文件中以b开头的所有命令有多少个,并将这些命令保存到b.txt文档中。将文档中以p结尾的所有命令保存到p.txt文件中,并统计有多少…

lv14 ioctl、printk及多个此设备支持 6

1 ioctl操作实现 对相应设备做指定的控制操作(各种属性的设置获取等等) long xxx_ioctl (struct file *filp, unsigned int cmd, unsigned long arg); 功能:对相应设备做指定的控制操作(各种属性的设置获取等等) 参数…

【csharp】依赖注入

依赖注入 依赖注入(Dependency Injection,DI)是一种软件设计模式,旨在降低组件之间的耦合度。在依赖注入中,一个类的依赖关系不是在类内部创建,而是通过外部传递进来。这通常通过构造函数、方法参数或属性…

氢燃料电池技术综述

文章目录 工作原理 系统集成 应用 特点 国家政策 行业发展 机遇和挑战 参考文献 工作原理 氢燃料电池是通过催化剂将氢气和氧气反应生成电能和水的过程,在这个过程中会伴随有热量产生。 系统集成 氢燃料电池需要将氢气供应系统、氧气供应系统、电堆、冷却系…

【基础篇】十二、引用计数法 可达性分析算法

文章目录 1、Garbage Collection2、方法区的回收3、堆对象回收4、引用计数法5、可达性分析算法6、查看GC Root对象 1、Garbage Collection C/C,无自动回收机制,对象不用时需要手动释放,否则积累导致内存泄漏: Java、C#、Python、…

Linux程序、进程以及计划任务(第一部分)

目录 一、程序和进程 1、什么是程序? 2、什么是进程? 3、线程是什么? 4、如何查看是多线程还是单线程 5、进程结束的两种情况: 6、进程的状态 二、查看进程信息的相关命令 1、ps:查看静态的进程统计信息 2、…

c++基础(对c的扩展)

文章目录 命令空间引用基本本质引用作为参数引用的使用场景 内联函数引出基本概念 函数补充默认参数函数重载c中函数重载定义条件函数重载的原理 命令空间 定义 namespace是单独的作用域 两者不会相互干涉 namespace 名字 { //变量 函数 等等 }eg namespace nameA {int num;v…

判断对象是否是垃圾的引用计数法有什么问题

给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加一,当引用失效计数器就减一,任何时候引用计数器为0的对象就是不可能再被使用的(变成垃圾)。 这个方法实现简单、效率高,但是目前主…