一般来说内存屏障分为两层:编译器屏障和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. minipc安装与设置Ubuntu

    此文章是对刚刚在某宝买的minipc进行的Ubuntu server安装,以及部分应用过程 安装Ubuntu server22 参考一文搞懂Ubuntu Server 22.04.2安装 问题记录 开 ...

  2. 2023-7-27WPF的ContextMenu的传参绑定方式

    WPF的ContextMenu的绑定方式 [作者]长生 ContextMenu为何不能正常绑定 在wpf中ContextMenu和ToolTip一样都是弹出层,与VisualTree已经分离了,只不过 ...

  3. 自用gulp打包脚本,压缩html,压缩js,压缩css,压缩图片,功能齐全

    const gulp = require('gulp'); const fs = require('fs'); const htmlmin = require('gulp-htmlmin'); con ...

  4. WPF 全局样式资源管理

    在WPF通常我们习惯于把样式直接写在控件属性上,例如: <TextBox x:Name="pluginPathTxt" Margin="0,0,0,0" ...

  5. Vue Cli起别名

    vue cli 3的写法 module.exports = { configureWebpack: { resolve:{ extensions:[], alias:{ 'assets':'@/ass ...

  6. Django: AssertionError: `HyperlinkedIdentityField` requires the request in the serializer context. Add `context={'request': request}` when instantiating the serializer.

    错误翻译 AssertionError: ' HyperlinkedIdentityField '需要在序列化器上下文中请求.在实例化序列化器时添加' context={'request': requ ...

  7. Builder 生成器模式简介与 C# 示例【创建型2】【设计模式来了_2】

    〇.简介 1.什么是生成器模式? 一句话解释:   在构造一个复杂的对象(参数多且有可空类型)时,通过一个统一的构造链路,可选择的配置所需属性值,灵活实现可复用的构造过程. 生成器模式的重心,在于分离 ...

  8. 查看Linux系统下CPU、内存、硬盘等信息

    做个记录,方便日后查看使用. 1. 查看CPU 1.1 查看CPU个数 [root@qy-ggyf-zyl-31 ~]# cat /proc/cpuinfo | grep "physical ...

  9. 通过jmeter上传/导入文件

    系统性能测试,需要模拟生产环境搭建应用服务和建造环境数据,最大限度的还原生产环境,使系统性能测试的指标更加合乎实际,真实.准确. 如某项目财务系统中的薪资管理模块做工资计算的压测,需要在系统内造179 ...

  10. Redhat 8.2 系统语言切换(英文转中文)

    前提条件 确保已连上网,并且配好 yum 源 若未配好 yum 源 可参考我上一篇文章 部分 Linux 换国内源 操作步骤 安装中文语言包 dnf install glibc-langpack-zh ...