详述Java内存屏障,透彻理解volatile
一般来说内存屏障分为两层:编译器屏障和CPU屏障,前者只在编译期生效,目的是防止编译器生成乱序的内存访问指令;后者通过插入或修改特定的CPU指令,在运行时防止内存访问指令乱序执行。
下面简单说一下这两种屏障。
1、编译器屏障
编译器屏障如下:
asm volatile("": : :"memory")
内联汇编时只是插入了一个空指令"",关键在在内联汇编中的修改寄存器列表中指定了"memory",它告诉编译器:这条指令(其实是空的)可能会读取任何内存地址,也可能会改写任何内存地址。那么编译器会变得保守起来,它会防止这条fence命令上方的内存访问操作移到下方,同时防止下方的操作移到上面,也就是防止了乱序,是我们想要的结果。这条命令还有另外一个副作用:它会让编译器把所有缓存在寄存器中的内存变量刷新到内存中,然后重新从内存中读取这些值。
总结一下就是,如上命令有两个作用,防止指令重排序以及保证可见性。
如果使用纯字节码解释器来运行Java,那么HotSpot VM中orderAccess_linux_zero.inline.hpp文件中有如下实现:
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(); }
这种方式依赖于编译器达到目的时,如果编译器支持,就不用在不同的平台和CPU上再专门编写对应的实现,简化了跨平台操作。
2、x86 CPU屏障
x86属于一个强内存模型,这意味着在大多数情况下CPU会保证内存访问指令有序执行。为了防止这种CPU乱序,我们需要添加CPU内存屏障。X86专门的内存屏障指令是"mfence",另外还可以使用lock指令前缀起到相同的效果,后者开销更小。也就是说,内存屏障可以分为两类:
- 本身是内存屏障,比如“lfence”,“sfence”和“mfence”汇编指令
- 本身不是内存屏障,但是被lock指令前缀修饰,其组合成为一个内存屏障。在X86指令体系中,其中一类内存屏障常使用“lock指令前缀加上一个空操作”方式实现,比如lock addl $0x0,(%esp)
下面介绍一下lock指令前缀。lock指令前缀功能如下:
- 被修饰的汇编指令成为“原子的”
- 与被修饰的汇编指令一起提供内存屏障效果
在X86指令体系中,具有lock指令前缀,其内允许使用lock指令前缀修饰的汇编指令有:
ADD,ADC,AND,BTC,BTR,BTS,CMPXCHG,CMPXCH8B,DEC,INC,NEG,NOT,OR,SBB,SUB,XOR,XADD,XCHG等
需要注意的是,“XCHG”和“XADD”汇编指令本身是原子指令,但也允许使用lock指令前缀进行修饰。
lock前缀的2个作用要记住。第一个是内存屏障,任何显式或隐式带有lock前缀的指令以及CPUID等指令都有内存屏障的作用。如xchg [mem], reg具有隐式的lock前缀。第二个是原子性,单指令并不是一个不可分割的操作,比如mov,本身只有其操作数满足某些条件的时候才是原子的,但是如果允许有lock前缀,那就是原子的。
3、HotSpot VM中的内存屏障
JMM为了更好让Java开发者独立于CPU的方式理解这些概念,对内存读(Load)和写(Store)操作进行两两组合:LoadLoad、LoadStore、StoreLoad以及StoreStore,只有StoreLoad组合可能乱序,而且Store和Load的内存地址必须是不一样的。
现在只讨论x86架构下的CPU屏障,参考的是Intel手册。4个屏障只是Java为了跨平台而设计出来的,实际上根据CPU的不同,对应 CPU 平台上的 JVM 可能可以优化掉一些 屏障,例如LoadLoad、LoadStore和StoreStore是x86上默认就有的行为,在这个平台上写代码时会简化一些开发过程。X86-64下仅支持一种指令重排:StoreLoad ,即读操作可能会重排到写操作前面,同时不同线程的写操作并没有保证全局可见,例子见《Intel 64 and IA-32 Architectures Software Developer’s Manual》手册8.6.1、8.2.3.7节。这个问题用lock或mfence解决,不能靠组合sfence和lfence解决。
JDK 1.8版本中的HotSpot VM在x86上实现的loadload()、storestore()以及loadstore()函数如下:
inline void OrderAccess::loadload(){
acquire();
}
inline void OrderAccess::storestore(){
release();
}
inline void OrderAccess::loadstore(){
acquire();
}
inline void OrderAccess::storeload(){
fence();
}
inline void OrderAccess::acquire() {
volatile intptr_t local_dummy;
#ifdef AMD64
__asm__ volatile ("movq 0(%%rsp), %0" : "=r" (local_dummy) : : "memory");
#else
__asm__ volatile ("movl 0(%%esp),%0" : "=r" (local_dummy) : : "memory");
#endif // AMD64
}
inline void OrderAccess::release() {
// Avoid hitting the same cache-line from different threads.
volatile jint local_dummy = 0;
}
acquire语义防止它后面的读写操作重排序到acquire前面,所以LoadLoad和LoadStore组合后可满足要求;release防止它前面的读写操作重排序到release后面,所以可由StoreStore和LoadStore组合后满足要求。这样acquire和release就可以实现一个"栅栏",禁止内部读写操作跑到外边,但是外边的读写操作仍然可以跑到“栅栏”内。
在x86上,acquire和release没有涉及到StoreLoad,所以本来默认支持,在函数实现时,完全可以不做任何操作。具体在实现时,acquire()函数读取了一个C++的volatile变量,而release()函数写入了一个C++的volatile变量。这可能是支持微软从Visual Studio 2005开始就对C++ volatile关键字添加了同步语义,也就是对volatile变量的读操作具有acquire语义,对volatile变量的写操作具有release语义。
另外还可以顺便说一下,借助acquire与release语义可以实现互斥锁(mutex),实际上,mutex正是acquire与release这两个原语的由来,acquire的本意是acquire a lock,release的本意是release a lock,因此,互斥锁能保证被锁住的区域内得到的数据不会是过期的数据,而且所有写入操作在release之前一定会写入内存。所以后续我们在实现锁的过程中会有如下代码出现:
pthread_mutex_lock(&mutex);
// 操作
pthread_mutex_unlock(&mutex);
OrderAccess::storeload()函数调用的fence()的实现如下:
inline void OrderAccess::fence() {
__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
}
可以看到是使用lock前缀来解决内存屏障问题。
下面看一下Java的volatile变量的实现。
字节码层面会在access_flags中会标记某个属性为volatitle,到HotSpot VM后,对volatitle内存区进行读写时,都加屏障,如读取volatile变量时加如下屏障:
volatile变量读操作
LoadLoad
LoadStore
在写volatilie变量时加如下屏障:
LoadStore
StoreStore
volatile变量写操作
StoreLoad
如上的volatile变量读之后的操作不允许重排序到前面,而写之前的操作也不允许重排序到写后面,所以volatile有acquire和release的语义。
对x86-64位来说,只需要对StoreLoad进行处理,所以从解释执行的putfield或putstatic指令来看(可参考文章:第26篇-虚拟机对象操作指令之putstatic),会在最后写入volatilie变量后加如下指令:
lock addl $0x0,(%rsp)
在Java的synchronized过程中,也要保证可见性及乱序行为。 写单线程代码的程序员不需要关心内存乱序的问题。在多线程编程中,由于使用互斥量,信号量和事件都在设计的时候都阻止了它们调用点中的内存乱序(已经隐式包含各种memery barrier),内存乱序的问题同样不需要考虑了。只有当使用无锁(lock-free)技术时–内存在线程间共享而没有任何的互斥量,内存乱序的效果才会显露无疑,这样我们才需要考虑在合适的地方加入合适的memery barrier。
B站上已经更新出了一系列的课程,关于一个手写Hotspot VM的课程,超级硬核,从0开始写HotSpot VM,将HotSpot VM所有核心的实现全部走一遍,有兴趣可关注B站Up主。

详述Java内存屏障,透彻理解volatile的更多相关文章
- 全面理解Java内存模型(JMM)及volatile关键字(转载)
关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java类加载器(ClassLoad ...
- 全面理解Java内存模型(JMM)及volatile关键字
[版权申明]未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) http://blog.csdn.net/javazejian/article/details/72772461 出自[zejian ...
- 全面理解Java内存模型(JMM)及volatile关键字(转)
原文地址:全面理解Java内存模型(JMM)及volatile关键字 关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型( ...
- 深入理解Java内存模型JMM与volatile关键字
深入理解Java内存模型JMM与volatile关键字 多核并发缓存架构 Java内存模型 Java线程内存模型跟CPU缓存模型类似,是基于CPU缓存模型来建立的,Java线程内存模型是标准化的,屏蔽 ...
- java内存屏障
为什么会有内存屏障 每个CPU都会有自己的缓存(有的甚至L1,L2,L3),缓存的目的就是为了提高性能,避免每次都要向内存取.但是这样的弊端也很明显:不能实时的和内存发生信息交换,分在不同CPU执行的 ...
- Java内存模型之分析volatile
前篇博客[死磕Java并发]—–深入分析volatile的实现原理 中已经阐述了volatile的特性了: volatile可见性:对一个volatile的读,总可以看到对这个变量最终的写: vola ...
- JVM Java 内存区域透彻分析(转)
出处: Java 内存区域透彻分析 Java8内存模型—永久代(PermGen)和元空间(Metaspace) 这篇文章主要介绍Java内存区域,也是作为Java虚拟机的一些最基本的知识,理解了这 ...
- 深入理解 Java 内存模型 JMM 与 volatile
Java 内存模型(Java Memory Model,简称 JMM)是一种抽象的概念,并不真实存在,它描述的是一组规范或者规则,通过这种规范定义了程序中各个变量(包括实例字段.静态字段和构成数组对象 ...
- java 内存模型的理解
之前一直在实习,博客停写了一段时间,现在秋招开始了,所以辞职回来专心看书,同时将每天的收获以博客的形式记录下来.最近在看jvm相关的书籍,下面对面试中问得最多的部分--java 内存模型. 本篇博客大 ...
- Java并发编程:JMM (Java内存模型) 以及与volatile关键字详解
目录 计算机系统的一致性 Java内存模型 内存模型的3个重要特征 原子性 可见性 有序性 指令重排序 volatile关键字 保证可见性和防止指令重排 不能保证原子性 计算机系统的一致性 在现代计算 ...
随机推荐
- Code Generate V2.0 代码生成器
Code Generate 代码生成器 系统首页 使用说明 系统默认会根据SQL生成字段信息 className.fieldList.classComment 如下所示: 建表语句 CREATE TA ...
- Mysql基础5-用户及权限管理
一.介绍 DCL:Data Control Language(数据控制语言),用来管理数据库用户,控制数据库的访问,权限. 二.用户管理 1.查询用户 语法: 1.use mysql; 2.selec ...
- 树莓派使用Golang+MQ135检测室内空气质量
MQ135是一个比较便宜的空气质量传感器,可以用在家庭以及工业场所中.树莓派是一个小巧但很强大的卡片电脑,基于Linux,同时提供了很多硬件接口,方便开发出各种电子产品.Golang是一款简单高效 ...
- MASABlazor在移动端点击保持出现悬停样式
提出问题 最近在学习MAUIBlazor,用的MASA Blazor,发现在移动端(触屏设备)上,点击会一直显示悬停样式,如下图所示,简单研究了一下,发现这是移动端的通病. 解决问题 MASABlaz ...
- Linux 问题:网络相关
防火墙 同网段双网卡 双网关 看服务日志
- Linux 命令:grub2-mkconfig
检索这个命令的,肯定都知道 grub 是 bootloader 程序,用于引导系统启动.配置文件是 grub.conf,现在一般的 grub 版本是grub2. 当机器上安装有多个内核.或者多个操作系 ...
- WPF 中WebBrowser 控件的“允许阻止的内容”修复(引用本地的html页)
解决方法:(个人理解:导致原因就是iE安全机制的问题吧).在你的HTML里面第一行加: <!-- saved from url=(0014)about:internet -->具体原因可以 ...
- FJOI2022 游记
2022.3.28 省选延期,延到了4.16 2022.4.11 省选又延期,延到了5.2 FJOI 要回来了!! Day -7 开始停课了 QwQ Day -6 打摆 Day -5 打摆 不行,我不 ...
- [Lua][Love Engine] 打砖块游戏实现过程与知识点
本文旨在根据LOVE2D官方文档和教程实现打砖块的游戏,记录部分实现过程和重要知识点 目标摧毁所有砖块 玩家控制球拍左右滑动反弹小球 小球摧毁砖块 小球保持在屏幕内 小球碰到屏幕底部,GAME OVE ...
- LeetCode买卖股票之一:基本套路(122)
欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 关于<LeetCode买卖股票>系列 在L ...