一般来说内存屏障分为两层:编译器屏障和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的更多相关文章

  1. 全面理解Java内存模型(JMM)及volatile关键字(转载)

    关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java类加载器(ClassLoad ...

  2. 全面理解Java内存模型(JMM)及volatile关键字

    [版权申明]未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) http://blog.csdn.net/javazejian/article/details/72772461 出自[zejian ...

  3. 全面理解Java内存模型(JMM)及volatile关键字(转)

    原文地址:全面理解Java内存模型(JMM)及volatile关键字 关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型( ...

  4. 深入理解Java内存模型JMM与volatile关键字

    深入理解Java内存模型JMM与volatile关键字 多核并发缓存架构 Java内存模型 Java线程内存模型跟CPU缓存模型类似,是基于CPU缓存模型来建立的,Java线程内存模型是标准化的,屏蔽 ...

  5. java内存屏障

    为什么会有内存屏障 每个CPU都会有自己的缓存(有的甚至L1,L2,L3),缓存的目的就是为了提高性能,避免每次都要向内存取.但是这样的弊端也很明显:不能实时的和内存发生信息交换,分在不同CPU执行的 ...

  6. Java内存模型之分析volatile

    前篇博客[死磕Java并发]—–深入分析volatile的实现原理 中已经阐述了volatile的特性了: volatile可见性:对一个volatile的读,总可以看到对这个变量最终的写: vola ...

  7. JVM Java 内存区域透彻分析(转)

    出处:  Java 内存区域透彻分析  Java8内存模型—永久代(PermGen)和元空间(Metaspace) 这篇文章主要介绍Java内存区域,也是作为Java虚拟机的一些最基本的知识,理解了这 ...

  8. 深入理解 Java 内存模型 JMM 与 volatile

    Java 内存模型(Java Memory Model,简称 JMM)是一种抽象的概念,并不真实存在,它描述的是一组规范或者规则,通过这种规范定义了程序中各个变量(包括实例字段.静态字段和构成数组对象 ...

  9. java 内存模型的理解

    之前一直在实习,博客停写了一段时间,现在秋招开始了,所以辞职回来专心看书,同时将每天的收获以博客的形式记录下来.最近在看jvm相关的书籍,下面对面试中问得最多的部分--java 内存模型. 本篇博客大 ...

  10. Java并发编程:JMM (Java内存模型) 以及与volatile关键字详解

    目录 计算机系统的一致性 Java内存模型 内存模型的3个重要特征 原子性 可见性 有序性 指令重排序 volatile关键字 保证可见性和防止指令重排 不能保证原子性 计算机系统的一致性 在现代计算 ...

随机推荐

  1. Code Generate V2.0 代码生成器

    Code Generate 代码生成器 系统首页 使用说明 系统默认会根据SQL生成字段信息 className.fieldList.classComment 如下所示: 建表语句 CREATE TA ...

  2. Mysql基础5-用户及权限管理

    一.介绍 DCL:Data Control Language(数据控制语言),用来管理数据库用户,控制数据库的访问,权限. 二.用户管理 1.查询用户 语法: 1.use mysql; 2.selec ...

  3. 树莓派使用Golang+MQ135检测室内空气质量

      MQ135是一个比较便宜的空气质量传感器,可以用在家庭以及工业场所中.树莓派是一个小巧但很强大的卡片电脑,基于Linux,同时提供了很多硬件接口,方便开发出各种电子产品.Golang是一款简单高效 ...

  4. MASABlazor在移动端点击保持出现悬停样式

    提出问题 最近在学习MAUIBlazor,用的MASA Blazor,发现在移动端(触屏设备)上,点击会一直显示悬停样式,如下图所示,简单研究了一下,发现这是移动端的通病. 解决问题 MASABlaz ...

  5. Linux 问题:网络相关

    防火墙 同网段双网卡 双网关 看服务日志

  6. Linux 命令:grub2-mkconfig

    检索这个命令的,肯定都知道 grub 是 bootloader 程序,用于引导系统启动.配置文件是 grub.conf,现在一般的 grub 版本是grub2. 当机器上安装有多个内核.或者多个操作系 ...

  7. WPF 中WebBrowser 控件的“允许阻止的内容”修复(引用本地的html页)

    解决方法:(个人理解:导致原因就是iE安全机制的问题吧).在你的HTML里面第一行加: <!-- saved from url=(0014)about:internet -->具体原因可以 ...

  8. FJOI2022 游记

    2022.3.28 省选延期,延到了4.16 2022.4.11 省选又延期,延到了5.2 FJOI 要回来了!! Day -7 开始停课了 QwQ Day -6 打摆 Day -5 打摆 不行,我不 ...

  9. [Lua][Love Engine] 打砖块游戏实现过程与知识点

    本文旨在根据LOVE2D官方文档和教程实现打砖块的游戏,记录部分实现过程和重要知识点 目标摧毁所有砖块 玩家控制球拍左右滑动反弹小球 小球摧毁砖块 小球保持在屏幕内 小球碰到屏幕底部,GAME OVE ...

  10. LeetCode买卖股票之一:基本套路(122)

    欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://github.com/zq2599/blog_demos 关于<LeetCode买卖股票>系列 在L ...