文档章节

volatile再总结

Hosee
 Hosee
发布于 2018/11/22 17:06
字数 2012
阅读 245
收藏 1

之前转载过一篇关于volatile的文章(为什么volatile能保证可见性)。 回顾以后,发现有几个问题还没有解释清楚,这篇文章将更细的谈一下关于volatile的问题。相关内容引用,请查看Reference

问题

  1. volatile在转变成汇编后,会有lock前缀指令,为什么volatile需要lock?什么是lock前缀?
  2. 为什么有些介绍volatile的文章里只有lock前缀,没有内存屏障?内存屏障和lock前缀有什么关联和区别?

为什么会导致不可见

CPU缓存

CPU缓存的出现主要是为了解决CPU运算速度与内存读写速度不匹配的矛盾,因为CPU运算速度要比内存读写速度快得多,举个例子:

  • 一次主内存的访问通常在几十到几百个时钟周期
  • 一次L1高速缓存的读写只需要1~2个时钟周期
  • 一次L2高速缓存的读写也只需要数十个时钟周期

这种访问速度的显著差异,导致CPU可能会花费很长时间等待数据到来或把数据写入内存。 基于此,现在CPU大多数情况下读写都不会直接访问内存(CPU都没有连接到内存的管脚),取而代之的是CPU缓存,CPU缓存是位于CPU与内存之间的临时存储器,它的容量比内存小得多但是交换速度却比内存快得多。而缓存中的数据是内存中的一小部分数据,但这一小部分是短时间内CPU即将访问的,当CPU调用大量数据时,就可先从缓存中读取,从而加快读取速度。 按照读取顺序与CPU结合的紧密程度,CPU缓存可分为:

  • 一级缓存:简称L1 Cache,位于CPU内核的旁边,是与CPU结合最为紧密的CPU缓存
  • 二级缓存:简称L2 Cache,分内部和外部两种芯片,内部芯片二级缓存运行速度与主频相同,外部芯片二级缓存运行速度则只有主频的一半
  • 三级缓存:简称L3 Cache,部分高端CPU才有

每一级缓存中所存储的数据全部都是下一级缓存中的一部分,这三种缓存的技术难度和制造成本是相对递减的,所以其容量也相对递增。 当CPU要读取一个数据时,首先从一级缓存中查找,如果没有再从二级缓存中查找,如果还是没有再从三级缓存中或内存中查找。一般来说每级缓存的命中率大概都有80%左右,也就是说全部数据量的80%都可以在一级缓存中找到,只剩下20%的总数据量才需要从二级缓存、三级缓存或内存中读取。

使用CPU缓存带来的问题

用一张图表示一下CPU–>CPU缓存–>主内存数据读取之间的关系:

当系统运行时,CPU执行计算的过程如下:

  1. 程序以及数据被加载到主内存
  2. 指令和数据被加载到CPU缓存
  3. CPU执行指令,把结果写到高速缓存
  4. 高速缓存中的数据写回主内存

如果服务器是单核CPU,那么这些步骤不会有任何的问题,但是如果服务器是多核CPU,那么问题来了,以Intel Core i7处理器的高速缓存概念模型为例(图片摘自《深入理解计算机系统》):

试想下面一种情况:

  1. 核0读取了一个字节,根据局部性原理,它相邻的字节同样被被读入核0的缓存
  2. 核3做了上面同样的工作,这样核0与核3的缓存拥有同样的数据
  3. 核0修改了那个字节,被修改后,那个字节被写回核0的缓存,但是该信息并没有写回主存
  4. 核3访问该字节,由于核0并未将数据写回主存,数据不同步

为了解决这个问题,CPU制造商制定了一个规则:当一个CPU修改缓存中的字节时,服务器中其他CPU会被通知,它们的缓存将视为无效。于是,在上面的情况下,核3发现自己的缓存中数据已无效,核0将立即把自己的数据写回主存,然后核3重新读取该数据。

反汇编Java字节码,查看汇编层面对volatile关键字做了什么

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

Java代码:


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

汇编代码:


0x01a3de1d: movb $0x0,0x1104800(%esi);

0x01a3de24: lock addl $0x0,(%esp);

addl $0x0,(%esp)(把ESP寄存器的值加0)显然是一个空操作(采用这个空操作而不是空操作指令nop是因为IA32手册规定lock前缀不允许配合nop指令使用),关键在于lock前缀,查询IA32手册,它的作用使得本CPU的Cache写入内存,该写入动作也会引起别的CPU或者别的内核无效化(Invalidate)其Cache。

x86中通过lock; addl $0,0(%%esp)这样的空操作来实现StoreLoad。 lock前缀的意义与前文提到的内存屏障一样。详见https://stackoverflow.com/questions/40409297/does-lock-xchg-have-the-same-behavior-as-mfence

相信有了上面对于lock的解释,volatile关键字的实现原理应该是一目了然了。首先看一张图:

工作内存Work Memory其实就是对CPU寄存器和高速缓存的抽象,或者说每个线程的工作内存也可以简单理解为CPU寄存器和高速缓存。

那么当写两条线程Thread-A与Threab-B同时操作主存中的一个volatile变量i时,Thread-A写了变量i,那么:

  • Thread-A发出LOCK#指令
  • 发出的LOCK#指令锁总线(或锁缓存行),同时让Thread-B高速缓存中的缓存行内容失效
  • Thread-A向主存回写最新修改的i

Thread-B读取变量i,那么:

  • Thread-B发现对应地址的缓存行被锁了,等待锁的释放,缓存一致性协议会保证它读取到最新的值

由此可以看出,volatile关键字的读和普通变量的读取相比基本没差别,差别主要还是在变量的写操作上。

总结

总结一下内存屏障在volatile中的作用:

  1. 保证有序性,内存屏障之前一定比内存屏障之后的先执行,不会被指令重排
  2. 保证可见性,内存屏障(lock前缀)可将变量所在缓存行的数据会立即写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

Reference:

  1. http://www.importnew.com/27002.html
  2. http://zhizus.com/2018-08-26-%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BAvolatile.html
  3. https://zhuanlan.zhihu.com/p/29868853

© 著作权归作者所有

Hosee
粉丝 621
博文 135
码字总数 209956
作品 0
杭州
程序员
私信 提问
加载中

评论(0)

【多线程总结(四)-三大性质总结】

前言 在并发编程中分析线程安全的问题时三条性质:原子性,有序性和可见性往往是非常重要的,本篇博客主要来用synchronized和volatile关键来进行对比。首先来看看宏观导图 核心 原子性 原子性...

我是太阳啦啦啦
2018/09/16
0
0
java中的volatile关键字的总结使用

volatile的作用 a.volatile关键字可以简单保持赋值和返回操作的原子性,弱同步。 比如:读取和写入long和double不是原子性的操作,jvm会把64位(long和double)的读取和写入当作两个分离的3...

双月通天
2016/06/01
66
0
ConcurrentHashMap比其他并发集合的安全效率要高一些?

前言 我们知道,ConcurrentHashmap(1.8)这个并发集合框架是线程安全的,当你看到源码的get操作时,会发现get操作全程是没有加任何锁的,这也是这篇博文讨论的问题——为什么它不需要加锁呢?...

Java架构师追风
2019/09/04
0
0
volatile关键字的疑惑

最近在总结的时候对volatile关键字产生了疑惑,有三个问题 1. 比如我用volatile关键字来修饰一个自定义的Student类,而Student类中有一个非volatile的变量name,那么如果我对某个Student实例...

软件路上的小小白
2017/08/11
220
5
vilatile用法总结

根据c/c++语法,const可以出现的地方,volatile几乎也都可以出现。 但是,const修饰的对象其值不能改变,而volatile修饰的对象其值可以随意地改变,也就是说,volatile对象值可能会改变,即使...

文艺小青年
2017/06/07
0
0

没有更多内容

加载失败,请刷新页面

加载更多

长沙哪里有开餐饮费发票

电薇13564998196陈晨100 % 真从主业来看,2019年众诚保险围绕车险业务采取增设分支机构、加强合作、优化用户体验等动作,但综合成本率仍有所上行,业内指出,车险的价格透明度属天然属性,利...

岍票微fp2090
17分钟前
27
0
武汉哪里有开餐饮费发票

电薇13564998196陈晨100 % 真从主业来看,2019年众诚保险围绕车险业务采取增设分支机构、加强合作、优化用户体验等动作,但综合成本率仍有所上行,业内指出,车险的价格透明度属天然属性,利...

枅票嶶fp2090
19分钟前
15
0
南京哪里有开餐饮费发票

电薇13564998196陈晨100 % 真从主业来看,2019年众诚保险围绕车险业务采取增设分支机构、加强合作、优化用户体验等动作,但综合成本率仍有所上行,业内指出,车险的价格透明度属天然属性,利...

枅票微fp2090
20分钟前
27
0
Hibernate 5 测试的时候日志错误

运行 Hibernate 测试的时候错误提示: log4j:WARN No appenders could be found for logger (org.jboss.logging).log4j:WARN Please initialize the log4j system properly.java.lang......

honeymoose
35分钟前
22
0
略谈分布式系统中的容器设计模式

本文作者:zytan_cocoa 略谈分布式系统中的容器设计模式 谭中意 2020/3/5 前言:云原生(Cloud Native)不仅仅是趋势,更是现在进行时,它是构建现代的,可弹性伸缩的,快速迭代的计算网络服...

百度开发者中心
03/11
21
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部