voliate底层原理分析

  • volatile的作用
  • 硬件层面分析
  • 软件层面
  • volatile分析

volatile的作用

volatile的作用是保证被它修饰的共享变量具有可见性和有序性。

什么是内存可见性?
在多线程下一个线程修改了共享变量,能够立即让其他线程知道这个新值。

什么是有序性?
程序在执行时会以到达更高执行效率为目的,在保证最终程序结果正确的前提下进行指令重排,有序性就是阻止指令重排,保证指令按序执行。

硬件层面分析

我们先了解下硬件层面实现防止指令重排和内存可见性的原理。

CPU缓存的出现
计算机在执行程序指令时涉及到数据的读写,而程序运行时需要的临时数据都存在主存中,这样会导致一个问题,cpu执行的速度非常快而内存读写数据的速度较慢,这样就会导致指令处理效率低下。
为了解决这个问题cpu缓存出现了(cache memory),cpu缓存读取速度快,当程序运行时将需要的数据拷贝一份到cpu缓存,cpu直接与缓存交互,程序结束后再将缓存内的结果刷回到主存,保证了cpu执行效率。
现在的的cpu基本都是采用的三级缓存,如下图:
一级二级缓存是核心独享,三级缓存是核心共享。

缓存一致性
当加入了cpu缓存后性能的确得到了提升,但是当在多线程时因为每个线程中都会有一份数据缓存,当其中一个线程修改了这个数据并同步到主存,其它线程并不知道,这时就会导致缓存不一致,缓存不一致就会导致程序的最终结果错误。

例如:假设有两个线程同时对变量i进行加1操作,假设主存中i=1、线程的缓存中i=1,当线程1对变量i加1后自己缓存的i=2,刷回主存此时主存的i=2,此时线程2对自己缓存的i加1 i=2,刷回主存主存的i=2,正常情况下两个线程都对i加1,此时i应该等于3.

解决缓存不一致带来的问题有两种方案,加总线锁,使用缓存一致性协议。
1.cpu和其他的组件通信都是通过总线(控制总线、地址总线、数据总线 ),所以解决缓存不一致带来的问题可以加总线锁,这样就可以保证其他cpu阻塞不能访问缓存了该数据内存地址的缓存,但是这样操作相当于串行化,代价有点大,所以就引入缓存锁,不锁总线而是将缓存上锁。

2.缓存一致性协议就是当某块cpu对缓存中的数据进行操作后,再写回主存时通知其他cpu将自己缓存中的该数据视为无效,从主存中重新读取。
缓存一致性的实现基于缓存一致性协议,如MESI。

存储缓存和存储转发
通过上面的解决方案已经实现了cpu执行指令速度的提升以及缓存一致,但是缓存一致性协议是基于四个状态的切换(Modified、Exclusive、Share、Invalid),这就需要各个CPU之间进行交互(一个cpu改变共享变量通知其他cpu缓存中的该变量失效,然后等到失效成功消息回复给它在继续执行指令,在等待过程中就会造成cpu的阻塞——相当于同步处理),在这个过程中也会导致时延降低cpu的处理效率。

所以存储缓存和存储转发就出现了,

存储缓存(Store Buffer)是让上面的同步处理变为异步处理,它是在cpu和cpu缓存之间,思想就是让cpu把要写入缓存的数据先写入store buffer中,然后它就可以去执行其他指令,当接收其他cpu的回复消息时再把存储缓存中的数据写入缓存,然后在清除掉存储缓存中数据,这样就可以使cpu不用阻塞。

存储转发(Store Forward)是当cpu需要数据时先查看存储缓存中有无数据,如果有直接在里面读取,如果没有则去缓存中读取,这样做可以解决这样的问题:当一个cpu写入数据到存储缓存,但是存储缓存还没有写入缓存时,cpu再次用到这个数据读取的是缓存中的旧值导致错误。

还有什么地方需要优化吗?

当然有,我们可以想象当其他cpu接受到失效指令(某个cpu0发送的让共享变量失效),但是此时其他的cpu在处理某些指令不能立即发送回复失效指令回去,那这个失效指令就陷入等待,并且存储缓存又很小,长时间接受不到其他cpu回复很可能就满了。要解决这个问题就要引入“失效队列”
失效队列的思想:让失效指令进入到失效队列,然后其他cpu知道失效队列中有这个失效指令就可以发送回复消息给cpu0,不用让失效指令陷入阻塞。

通过上面的一系列缓存优化,似乎已经很完美了,那是在多线程并发处理的过程中还存在问题。
现在我们来想这样一个问题:

 1.cpu_0将共享变量写入store buffer后发生失效指令给其他cpu_1;2.cpu_1,没有先处理失效指令而是先处理其他指令暂时没办法响应这个失效指令,所以失效指令在失效队列中;3.此时cpu1处理的指令正巧需要这个共享变量,而因为它没有相应这失效指令所以自己缓存中的变量值仍然有效,就会使用这个旧值;4.cpu1处理完其他指令后相应这个失效指令,但是此时已经晚了,上面已经用了旧值。

上面的现象很明显就是指令错乱执行,导致后面的指令屏蔽了前面指令的结果——这就是cpu的指令重排。当然程序执行时还会涉及到编译重排,原理都是差不多了,都是为了更高效的执行在某种规则下重排执行顺序。
编译器重排遵循的规则:happens-before规则和as-if-serial规则
happens-before规则

1.程序次序规则: 在一个单独的线程中,按照程序代码的执行流顺序,(时间上)先执行的        操作happen—before(时间上)后执行的操作(同一个线程中前面的所有写操作对后面的操作可见)2.管理锁定规则:一个unlock操作happen—before后面(时间上的先后顺序)对同一个锁的lock操作。(如果线程1解锁了monitor a,接着线程2锁定了a,那么,线程1解锁a之前的写操作都对线程2可见(线程1和线程2可以是同一个线程))3.volatile变量规则:对一个volatile变量的写操作happen—before后面(时间上)对该变量的读操作。(如果线程1写入了volatile变量v(临界资源),接着线程2读取了v,那么,线程1写入v及之前的写操作都对线程2可见(线程1和线程2可以是同一个线程))4.线程启动规则:Thread.start()方法happen—before调用用start的线程前的每一个操作。
(假定线程A在执行过程中,通过执行ThreadB.start()来启动线程B,那么线程A对共享变量的修改在接下来线程B开始执行前对线程B可见。注意:线程B启动之后,线程A在对变量修改线程B未必可见。)5.线程终止规则:线程的所有操作都happen—before对此线程的终止检测,可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
(线程t1写入的所有变量,在任意其它线程t2调用t1.join(),或者t1.isAlive() 成功返回后,都对t2可见。)6.线程中断规则:对线程interrupt()的调用 happen—before 发生于被中断线程的代码检测到中断时事件的发生。
(线程t1写入的所有变量,调用Thread.interrupt(),被打断的线程t2,可以看到t1的全部操作)
对象终结规则:一个对象的初始化完成(构造函数执行结束)happen—before它的finalize()方法的开始。(对象调用finalize()方法时,对象初始化完成的任意操作,同步到全部主存同步到全部cache。)7.传递性:如果操作A happen—before操作B,操作B happen—before操作C,那么可以得出A happen—before操作C。

as-if-serial规则

 不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改
变。编译器,runtime 和处理器都必须遵守as-if-serial语义,(把单线程程序保护了起来)。
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这
种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作可能被编译器
和处理器重排序。

CPU内存屏障
因为CPU指令重排导致的问题引入内存屏障解决

写屏障(store memory barrier):看到写屏障就是要求写屏障前面的所有写指令执行完毕才能继续执行,保证写入最新数据。

读屏障(load memory barrier):读屏障之后的读操作都要在读屏障之后执行,保证读取到最新的数据。

全屏障(full memory barrier):兼备读写屏障的功能。
内存屏障防止cpu乱序执行(指令重排)带来问题。

以上就是硬件层面实现内存可见性以及防止指令重排。

软件层面

下面我们来分析软件层面怎么实现可见性和有序性

在JAVA中实现了java内存模型(JMM),它将硬件层面的的问题进行了抽象,屏蔽了不同平台硬件之间的差异,通过实现了JMM层面的内存屏障解决指令重排问题,通过将内存分为主内存和线程工作内存按照一定规则控制交互实现内存可见性。

主内存所有线程共享,工作内存每个线程独享。工作内存中有一份主内存共享变量的副本。

JMM中规定所有的变量都存储在主内存(Main Memory)中,每条线程都有自己的工作内存(Work Memory),线程的工作内存中保存了该线程所使用的变量的从主内存中拷贝的副本。线程对于变量的读、写都必须在工作内存中进行,而不能直接读、写主内存中的变量。同时,本线程的工作内存的变量也无法被其他线程直接访问,必须通过主内存完成。

JMM层面的内存屏障:

1.StoreStore屏障:Store1 StoreStore Store2,保证Store1的存储指令(刷新到内存)先于Store2以及所有后续的存储指令。

2.StoreLoad屏障:Store1 StoreLoad Load1,保证Store1的存储指令(刷新到内存)先于Load1以及所有后续的装载指令。

3.LoadLoad屏障:Load1 LoadLoad Load2,保证Load1的装载指令先于Load2以及所有后续的装载指令。

4.LoadStore屏障:Load1 LoadStore Store1,保证Load1装载数据先于Store1以及所有后续的存储指令。

通过上面的学习,我们已经了解了硬件和软件层面实现内存可见性和有序性的原理。

volatile分析

那么volatile又是怎么实现有序性和可见性的呢?

1.volatile通过在编译时插入lock前缀指令实现内存的可见性。

volatile修饰变量时,生成的汇编指令会比普通的变量声明会多一个Lock指令。有volatile变量修饰的共享变量进行写操作的时候,Lock指令会在多核处理器下会做两件事情。1.将当前处理器缓存行的数据直接写会到系统内存中(从Java内存模型来理解,就是将线程中的工作内存的数据直接写入到主内存中)2.这个写回内存的操作会使在其他CPU里缓存了该地址的数据无效(从Java内存模型理解,当线程A将工作内存的数据修改后(新值),同步到主内存中,那么线程B从主内存中初始的值(旧值)就无效了——这是通过硬件层面的缓存一致性协议实现的。)

2.volatile通过在其读写操作前后插入内存屏障来防止指令重排。

JMM定义了8种操做来完成:
1.lock(加锁): 作用于主内存,把一个变量标记为线程独占。
2.unlock(解锁):作用于主内存,把一个已锁定的变量释放出来。
3.read(读取):作用于主内存,将一个变量从主内从中传输到工做内存中,以便随后的load。
4.load(载入):作用于工作内存,把read操做获得的变量放在工作内存的变量副本中。
5.use(使用):做用于工作内存,把工作内存中的一个变量传递给执行引擎。
6.assign(赋值):做用于工作内存,把一个执行引擎接受的值赋值给工作内存的变量。
7.store(存储):作用于工作内存,把工作内存中的一个变量的值传输到主内存,以便后续的write操做。
8.write(写入):作用于主内存,把store操作从工作内存获得的值放回主内存中。

8中操做有以下关系:
1.不容许load和read,store和write单独出现。
2.不容许一个线程丢弃它最近的assign操做,即变量在工作内存中改变,必须同步回主内存。
3.不与许一个线程无缘由的(没有assign操做)把数据从工作内存同步回主内存。
4.一个新的变量只能在主内存中诞生。
5.一个变量只能同时有一个线程进行加锁。lock能够被同一个线程加锁屡次,可是必须解锁相同次数。这个变量才会被解锁。
6.对一个变量执行lock操做。将会先清空该线程的工作内存中的该变量的值。在执行引擎使用这个变量前,须要从新执行load或assign操做。
7.一个变量被lock,不容许其它线程执行unlock。也不容许执行unlock被别的线程lock的变量。即一个线程本身lock的只有本身能unlock.
8.一个变量unlock以前,工作内存中的数据必须同步回主内存。
这八种操做和其使用规则,决定了变量在工作内存和主内存之间的同步策略。

针对于volatile变量又有额外以下定义:
volatile变量在use时,必须执行load操作,相当于把read load use 动作关联。即每次使用volatile变量必须先从主内存中刷新最新值。
volatile变量在assign时,必须执行write操作,相当于 wirte stroe assign 动作关联。即每次对volatile进行赋值操作必须立马同步回主内存。

前面已经说了程序编译也会进行编译器指令重排,那么JMM给出了如下规则。
volatile重排序规则表

1.若是第二个操做时volatile写操做,无论第一操做是什么操做,都不能重排。
2.若是第一个操做时volatile读操做,无论第二个操做时什么操做,都不能重排。
3.volatile写和volatile读不能重排。

为了实现这个语义,JVM在生成字节码时,会在指令序列中插入内存屏障(memory barrier)来禁止特定类型的处理器指令重排,对于编译器来讲对全部的CPU来插入屏障数最小的方案几乎不可能,下面是基于保守策略的JMM内存屏障插入策略:
1.在每个volatile写操作的前面插入一个StoreStore屏障
2.在每个volatile写操作的后面插入一个SotreLoad屏障
3.在每个volatile读操作的后面插入一个LoadLoad屏障
4.在每个volatile读操作的后面插入一个LoadStore屏障

如有错误欢迎指正,共同进步。
下篇文章分析重排表的规则!!!

voliate可见性和有序性原理——硬件以及软件层面分析相关推荐

  1. Spring原理学习系列之三:Spring AOP原理(从源码层面分析)-------上部

    引言 本文是Spring原理分析的第三篇博文,主要阐述Spring AOP相关概念,同时从源码层面分析AOP实现原理.对于AOP原理的理解有利于加深对Spring框架的深入理解.同时我也希望可以探究S ...

  2. 两个例子详解并发编程的可见性问题和有序性问题,通过volatile保证可见性和有序性以及volatile的底层原理——缓存一致性协议MESI和内存屏障禁止指令重排

    1. 并发编程的可见性问题 2. 并发编程的有序性问题 3. 使用volatile关键字解决可见性问题 4. 可见性问题的本质--缓存不一致 因为cpu执行速度很快,但是内存执行速度相对于CPU很慢, ...

  3. java中实现具有传递性吗_Java中volatile关键字详解,jvm内存模型,原子性、可见性、有序性...

    一.Java内存模型 想要理解volatile为什么能确保可见性,就要先理解Java中的内存模型是什么样的. Java内存模型规定了所有的变量都存储在主内存中.每条线程中还有自己的工作内存,线程的工作 ...

  4. volatile关键字——保证并发编程中的可见性、有序性

    文章目录 一.缓存一致性问题 二.并发编程中的三个概念 三.Java线程内存模型 1.原子性 2.可见性 3.有序性 四.深入剖析volatile关键字 1.volatile关键字的两层语义 2.vo ...

  5. Java并发编程—volatile关键字(保证变量的可见性、有序性机制)

    原文作者:Matrix海子 原文地址:Java并发编程:volatile关键字解析 volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程 ...

  6. c++的可见性,有序性与原子性

    c++的可见性,有序性与原子性 1.可见性   可见性指的当一个线程修改某个变量时,另一个线程也可以看到该变量的值的变化.但是由于编译器的优化或者cpu寄存器以及cache的硬件结构,有可能某个线程改 ...

  7. java基础—java内存模型(JMM)CPU架构、缓存一致性、重排序、JMM的实现、JMM保证可见性、有序性问题的详解

    java基础-JMM(CPU架构.JMM保证可见性.有序性) 文章目录 java基础-JMM(CPU架构.JMM保证可见性.有序性) CPU架构 说一说现在计算机存储器的结构层次 使用CPU缓存,会导 ...

  8. synchronized同时对原子性、可见性、有序性的保证

    原子性:基本复制写操作都能保证原子性,复杂操作无法保证 可见性:MESI协议的flush.refresh配合使用,解决可见性 有序性:3个层次,最后1个层次有4中内存重排序 synchronized可 ...

  9. JMM中的原子性、可见性、有序性和volatile关键字

    相信如果对JMM底层有过了解或者接触过java并发编程的读者对以上的概念并不陌生,但是真正理解的可能并不多.这里我就对这些概念再做一次讲解.相信读者多读几遍应该就有自己的理解,实在不理解也没关系,说明 ...

最新文章

  1. 独家 | 一文读懂PySpark数据框(附实例)
  2. 官方解读,谷歌“T5”模型,如何突破迁移学习局限,实现多基准测试SOTA
  3. 【 MATLAB 】通过案例学会编写一个 matlab 函数(小猫掉进山洞问题)
  4. 华为VC首次出手:投资2家半导体公司,带火5G建材“碳化硅”
  5. html5画布可以p图,HTML5图像适合发布在画布上
  6. mysql的慢查询日志
  7. mysql中修改表结构语法_MySQL表结构修改详解
  8. php如何随机显示图片,php中随机显示图片的函数代码_php
  9. Python asyncio库的学习和使用
  10. jquery 加载提示框
  11. uclient和thinkphp的class db的冲突
  12. 【路由器】Breed 介绍、刷入和使用
  13. 怎样才能在电脑上下载计算机一级考试软件?
  14. 魔兽代理又起风云:网易暴雪腾讯的利益博弈
  15. C++20 实现字符串类型的转换操作
  16. Python基础(七) | 文件、异常以及模块详解
  17. Python实现占用栅格地图的生成(Occupancy Grid Generation)
  18. 计算机表演赛分赛区决赛,国际儿童节计算机表演赛 天津分赛区决赛开始
  19. 网页回拨-Web CallBack
  20. 考考你的基础知识:C++ 文件操作ofstream、ifstream使用

热门文章

  1. 2019年三大主流前端框架比较,程序员会怎么选?
  2. postgres 数据库导入导出
  3. 查看Teams聊天/频道消息所占空间
  4. JavaScript 字符串插值
  5. oppo便签误删怎么办_oppo便签文件丢失(三招快速免费恢复)
  6. NGUI HUD text 代码示例
  7. Spring和Spring Framework的理解
  8. Mybatis -study
  9. python绘制生日快乐图片_祝自己生日快乐 | 利用Python和R分析一年写作
  10. 周记-20201012