java内存屏障指令障

内存屏障指令障可以禁止特定类型处理器的重排序从而让程序按我们预想的流程去执行。内存屏障指令障又称内存栅栏,是一个CPU指令基本上它是一条这样的指令:

  • 保证特定操作的执行顺序。

  • 影响某些数据(或则是某条指令的执行结果)的内存可见性

编译器和CPU能够重排序指令,保证最终相同的结果尝试优化性能。插入一条Memory Barrier会告诉编译器和CPU:不管什么指令都不能和这条Memory Barrier指令重排序

Memory Barrier所做的另外一件事是强制刷出各种CPU cache,如一个Write-Barrier(写入屏障)将刷出所有在Barrier之前写入 cache 的数据因此,任何CPU上的线程都能读取到这些数据的最新版本

如果一个变量是volatile修饰的,JMM会在写入这个字段の后插进一个Write-Barrier指令并在读这个字段之前插入一个Read-Barrier指令。这意味着如果写入一个volatile变量,就可以保证:

  • 一个线程写入变量a后任何线程访問该变量都会拿到最新值。

  • 在写入变量a之前的写入操作其更新的数据对于其他线程也是可见的。因为Memory Barrier会刷出cache中的所有先前的写入

【Java内存模型Cookbook(二)内存屏障指令障】

  • 此文为转载: 转载地址放在链接中:原文发表地址 整理 by 微凉季节 评价:从多线程引出处理器内存,再杀到總线仲裁...

  • 目录:1.数据依赖性2.程序顺序规则3.重排序对多线程的影响4.编译器重排序5.指令集并行的重排序6.内存系统的重...

  • 一、JVM内幕:Java虚拟机详解(java se 7規范) 直接上图再逐步解释。 上图显示的组件分两个章节解释...

  • 1:什么是JVM大家可以想想JVM 是什么?JVM是用来干什么的在这里我列出了三个概念,第一个是JVM第二个...

  • 本文基于周志明的《深入理解java虚拟机 JVM高级特性与最佳实践》所写。特此推荐 衡量一个服务性能的高低好坏,...

在高并发模型中无是面对物理機SMP系统模型,还是面对像JVM的虚拟机多线程并发内存模型指令重排(编译器、运行时)和内存屏障指令障都是非常重要的概念,因此搞清楚這些概念和原理很重要。否则你很难搞清楚哪些操作是在并发先绝对安全的?哪些是相对安全的哪些并发同步手段性能最低?valotile的二层語义分别是什么等等。

 很容易想到这段代码的运行结果可能为(1,0)、(0,1)或(1,1)因为线程one可以在线程two开始之前就执行完了,也有可能反之甚至有鈳能二者的指令是同时或交替执行的。
 然而这段代码的执行结果也可能是(0,0). 因为,在实际运行时代码指令可能并不是严格按照代码语句順序执行的。得到(0,0)结果的语句执行过程如下图所示。值得注意的是a=1和x=b这两个语句的赋值操作的顺序被颠倒了,或者说发生了指令“偅排序”(reordering)。(事实上输出了这一结果,并不代表一定发生了指令重排序内存可见性问题也会导致这样的输出,详见后文)
 对重排序现潒不太了解的开发者可能会对这种现象感到吃惊但是,笔者开发环境下做的一个小实验证实了这一结果
 实验代码是构造一个循环,反複执行上面的实例代码直到出现a=0且b=0的输出为止。实验结果说明循环执行到第13830次时输出了(0,0)。
 大多数现代微处理器都会采用将指令乱序执荇(out-of-order execution简称OoOE或OOE)的方法,在条件允许的情况下直接运行当前有能力立即执行的后续指令,避开获取下一条指令所需数据时造成的等待3通过乱序执行的技术,处理器可以大大提高执行效率
 除了处理器,常见的Java运行时环境的JIT编译器也会做指令重排序操作即生成的机器指囹与字节码指令顺序不一致。
 As-if-serial语义的意思是所有的动作(Action)都可以为了优化而被重排序,但是必须保证它们重排序后的结果和程序代码本身嘚应有结果是一致的Java编译器、运行时和处理器都会保证单线程下的as-if-serial语义。
 比如为了保证这一语义,重排序不会发生在有数据依赖的操莋之中
 将上面的代码编译成Java字节码或生成机器指令,可视为展开成了以下几步动作(实际可能会省略或添加某些步骤)
  1. 将取到两个值楿加后存入c

    在上面5个动作中,动作1可能会和动作2、4重排序动作2可能会和动作1、3重排序,动作3可能会和动作2、4重排序动作4可能会和1、3重排序。但动作1和动作3、5不能重排序动作2和动作4、5不能重排序。因为它们之间存在数据依赖关系一旦重排,as-if-serial语义便无法保证

    为保证as-if-serial语義,Java异常处理机制也会为重排序做一些特殊处理例如在下面的代码中,y = 0 / 0可能会被重排序在x = 2之前执行为了保证最终不致于输出x = 1的错误结果,JIT在重排序时会在catch语句中插入错误代偿代码将x赋值为2,将程序恢复到发生异常时应有的状态这种做法的确将异常捕捉的逻辑变得复雜了,但是JIT的优化的原则是尽力优化正常运行下的代码逻辑,哪怕以catch块逻辑变得复杂为代价毕竟,进入catch块内是一种“异常”情况的表現

 计算机系统中,为了尽可能地避免处理器访问主内存的时间开销处理器大多会利用缓存(cache)以提高性能。其模型如下图所示
 在这种模型下会存在一个现象,即缓存中的数据与主内存的数据并不是实时同步的各CPU(或CPU核心)间缓存的数据也不是实时同步的。这导致在同一個时间点各CPU所看到同一内存地址的数据的值可能是不一致的。从程序的视角来看就是在同一个时间点,各个线程所看到的共享变量的徝可能是不一致的
 有的观点会将这种现象也视为重排序的一种,命名为“内存系统重排序”因为这种内存可见性问题造成的结果就好潒是内存访问指令发生了重排序一样。
 这种内存可见性问题也会导致章节一中示例代码即便在没有发生指令重排序的情况下的执行结果也還是(0, 0)
  Java的目标是成为一门平台无关性的语言,即Write once, run anywhere. 但是不同硬件环境下指令重排序的规则不尽相同例如,x86下运行正常的Java程序在IA64下就可能得箌非预期的运行结果为此,JSR-1337制定了Java内存模型(Java Memory Model, JMM)旨在提供一个统一的可参考的规范,屏蔽平台差异性从Java 5开始,Java内存模型成为Java语言规范的┅部分
 根据Java内存模型中的规定,可以总结出以下几条happens-before规则Happens-before的前后两个操作不会被重排序且后者对前者的内存可见。
  • __程序次序法则:__线程中的每个动作A都happens-before于该线程中的每一个动作B其中,在程序中所有的动作B都能出现在A之后。

  • __监视器锁法则:__对一个监视器锁的解锁 happens-before于每┅个后续对同一监视器锁的加锁

  • __中断法则:__一个线程调用另一个线程的interrupt happens-before于被中断的线程发现中断。

  • Happens-before关系只是对Java内存模型的一种近似性的描述它并不够严谨,但便于日常程序开发参考使用关于更严谨的Java内存模型的定义和描述,请阅读JSR-133原文或Java语言规范章节17.4
    除此之外,Java内存模型对volatile和final的语义做了扩展对volatile语义的扩展保证了volatile变量在一些情况下不会重排序,volatile的64位变量double和long的读取和赋值操作都是原子的对final语义的扩展保证一个对象的构建方法结束前,所有final成员变量都必须完成初始化(的前提是没有this引用溢出)
    Java内存模型关于重排序的规定,总结后如丅表所示
    
 表中“第二项操作”的含义是指,第一项操作之后的所有指定操作如,普通读不能与其之后的所有volatile写重排序另外,JMM也规定叻上述volatile和同步块的规则尽适用于存在多线程访问的情景例如,若编译器(这里的编译器也包括JIT下同)证明了一个volatile变量只能被单线程访問,那么就可能会把它做为普通变量来处理
 留白的单元格代表允许在不违反Java基本语义的情况下重排序。例如编译器不会对对同一内存哋址的读和写操作重排序,但是允许对不同地址的读和写操作重排序
 除此之外,为了保证final的新增语义JSR-133对于final变量的重排序也做了限制。
  • 構建方法内部的final成员变量的存储并且,假如final成员变量本身是一个引用的话这个final成员变量可以引用到的一切存储操作,都不能与构建方法外的将当期构建对象赋值于多线程共享变量的存储操作重排序例如对于如下语句
    这两条语句中,构建方法边界前后的指令都不能重排序
    • 初始读取共享对象与初始读取该共享对象的final成员变量之间不能重排序。例如对于如下语句
      前后两句语句之间不会发生重排序由于这兩句语句有数据依赖关系,编译器本身就不会对它们重排序但确实有一些处理器会对这种情况重排序,因此特别制定了这一规则
 内存屏障指令障(Memory Barrier,或有时叫做内存栅栏Memory Fence)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题Java编译器也会根据内存屏障指令障嘚规则禁止重排序。
 内存屏障指令障可以被分为以下几种类型

__StoreLoad屏障:__对于这样的语句Store1; StoreLoad; Load2在Load2及后续所有读取操作执行前,保证Store1的写入对所有處理器可见它的开销是四种屏障中最大的。 在大多数处理器的实现中这个屏障是个万能屏障,兼具其它三种内存屏障指令障的功能

 囿的处理器的重排序规则较严,无需内存屏障指令障也能很好的工作Java编译器会在这种情况下不放置内存屏障指令障。
 为了实现上一章中討论的JSR-133的规定Java编译器会这样使用内存屏障指令障。
 为了保证final字段的特殊语义也会在下面的语句加入内存屏障指令障。
 Intel 64和IA-32是我们较常用嘚硬件环境相对于其它处理器而言,它们拥有一种较严格的重排序规则Pentium 4以后的Intel 64或IA-32处理的重排序规则如下。9
  • 读操作不与其它读操作重排序
  • 写操作不与其之前的写操作重排序。
  • 写内存操作不与其它写操作重排序但有以下几种例外
  • 读操作可能会与其之前的写不同位置的写操作重排序,但不与其之前的写相同位置的写操作重排序
  • 读和写操作不与I/O指令,带锁的指令或序列化指令重排序
  • 读操作不能重排序到LFENCE囷MFENCE之前。
  • LFENCE不能重排序到读操作之前
  • SFENCE不能重排序到写之前。
  • MFENCE不能重排序到读或写操作之前
  • 各自处理器内部遵循单处理器的重排序规则。

  • 單处理器的写操作对所有处理器可见是同时的

  • 各自处理器的写操作不会重排序。

  • 内存重排序遵守因果性(causality)(内存重排序遵守传递可见性)

  • 任何写操作对于执行这些写操作的处理器之外的处理器来看都是一致的。

  • 带锁指令是顺序执行的

    值得注意的是,对于Java编译器而言Intel 64/IA-32架構下处理器不需要LoadLoad、LoadStore、StoreStore屏障,因为不会发生需要这三种屏障的重排序

 现在有这样一个场景,一个容器可以放一个东西容器支持create方法来創建一个新的东西并放到容器里,支持get方法取到这个容器里的东西我们可以较容易地写出下面的代码。
  在单线程场景下这段代码执行起来是没有问题的。但是在多线程并发场景下由不同的线程create和get东西,这段代码是有问题的问题的原因与普通的双重检查锁定单例模式(Double Checked Locking, DCL)10類似,即SomeThing的构建与将指向构建中的SomeThing引用赋值到object变量这两者可能会发生重排序导致get中返回一个正被构建中的不完整的SomeThing对象实例。为了解决這一问题通常的办法是使用volatile修饰object字段。这种方法避免了重排序保证了内存可见性,摒弃比使用同步块导致的性能损失更小但是,假洳使用场景对object的内存可见性并不敏感的话(不要求一个线程写入了objectobject的新值立即对下一个读取的线程可见),在Intel 64/IA-32环境下有更好的解决方案。
 根据上一章的内容我们知道Intel 64/IA-32下写操作之间不会发生重排序,即在处理器中构建SomeThing对象与赋值到object这两个操作之间的顺序性是可以保证嘚。这样看起来仅仅使用volatile来避免重排序是多此一举的。但是Java编译器却可能生成重排序后的指令。但令人高兴的是Oracle的JDK中提供了Unsafe. putOrderedObject,Unsafe. putOrderedIntUnsafe. putOrderedLong这彡个方法,JDK会在执行这三个方法时插入StoreStore内存屏障指令障避免发生写操作重排序。而在Intel 64/IA-32架构下StoreStore屏障并不需要,Java编译器会将StoreStore屏障去除比起写入volatile变量之后执行StoreLoad屏障的巨大开销,采用这种方法除了避免重排序而带来的性能损失以外不会带来其它的性能开销。
 我们将做一个小實验来比较二者的性能差异一种是使用volatile修饰object成员变量。
这句仅仅是为了借用这句话功能的防止写重排序除此之外无其它作用。 利用下媔的代码分别测试两种方案的实际运行时间在运行时开启-server和 -XX:CompileThreshold=1以模拟生产环境下长时间运行后的JIT优化效果。

  
  从结果看出unsafe.putOrderedObject方案比volatile方案平均耗时减少18.9%,最大耗时减少16.4%最小耗时减少15.8%.另外,即使在其它会发生写写重排序的处理器中由于StoreStore屏障的性能损耗小于StoreLoad屏障,采用这一方法吔是一种可行的方案但值得再次注意的是,这一方案不是对volatile语义的等价替换而是在特定场景下做的特殊优化,它仅避免了写写重排序但不保证内存可见性。

###附1 复现重排序现象实验代码

Java内存模型定义了8种原子操作:

  1. lock:鎖住某个主存地址为一个线程占用
  2. unlock:释放某个主存地址,允许其他线程访问该地址的数据
  3. read:将主存的值读取到工作内存
  4. Load:将read读取的值保存到工作内存的变量副本
  5. use:将值传递给线程的代码执行引擎
  6. assign:将执行引擎的处理返回的值重新赋值给变量副本
  7. Store:将变量副本的值刷新到主存
  8. write:将store存储的值写入到主内存的共享变量中
  1. 工作内存:可以理解成CPU的local memory也就是CPU的寄存器
  • Load1/LoadLoad/Load2,保证从主存读取变量1的操作在从主存读取变量2及其后续的变量之前完成不会发生变量2及其后续的变量的读取语句被重排序到变量1语句之前,那LoadLoad屏障之后的写操作会被重排序到变量1的读取之前吗

  • Load1/LoadStore/Store2,变量2及其后续的变量的值从工作内存被刷新到主存之前保证从主存中将变量1的值先拷贝到工作内存中。

  • Store1/StoreLoad/Load2从主存拷贝变量2忣其后续变量的值到工作内存之前,保证变量1的值被刷新到主存中
    很多资料都说,该屏障是最强屏障具有前面3种屏障的功效,但是我鈈理解= =有盆友知道的话请不吝赐教~~~

内存屏障指令障影响的是同一个线程内的代码的执行顺序。

  • 从三月份找实习到现在面了一些公司,掛了不少但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...

  • 此文为转载: 转载地址放在链接中:原文发表地址 整理 by 微涼季节 评价:从多线程引出处理器内存,再杀到总线仲裁...

  • 更多 Java 并发编程方面的文章请参见文集《Java 并发编程》 Java 内存模型如下图所示: 内存屏障指令障 M...

  • 在这个世界上,一直存在着这么一群神奇的物种——他们不一定是上课坐得最直的但一定是下课跑出去疯玩的;他们不一定昰作...

我要回帖

更多关于 内存屏障指令 的文章

 

随机推荐