java内存模型之重排序

1.重排序

  在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序.重排序分三种类型:

  1.编译器优化的重排序.编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序.

  2.指令级并行的重排序.现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行.如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序.

  3.内存系统的重排序.由于处理器是使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是乱序执行.

  从java源代码到最终实际执行的指令序列,会分别经历下面三中重排序:

  

  上述的1属于编译器重排序,2和3属于处理器重排序.这些重排序都可能会导致多线程程序出现内存可见性问题.对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止).对于处理器重排序,JMM的处理器重排序规则会要求java编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,inter称之为memory fence)指令,通过特点的内存屏障来禁止特定的处理器重排序(不是所有的处理器重排序都要禁止).

  JMM属于语言级的内存模型,他确保在不同的编译器和不同的处理器平台上,通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性.

2.处理器重排序和内存屏障指令

  现代的处理器使用写缓冲区来临时保存向内存写入的数据.写缓冲区可以保证指令流水线持续运行,他可以避免由于处理器停顿下来等待向内存写入数据而产生的延迟.同时,通过以批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一内存地址的多次写,可以减少对内存总线的占用.虽然写缓冲区有这么多好处,但每个处理器上等的写缓冲区,仅仅对它所在的处理器可见.这个特性会对内存操作的执行顺序产生重要的影响:处理器对内存的读/写操作的执行顺序,不一定与内存实际发生的读/写操作顺序一致!

  

   假设处理器A和处理器B按程序的顺序并行执行内存访问,最终却可能得到x=y=0的结果.具体的原因如下所示:

  

  这里处理器A和处理器B可以同时把共享变量写入自己的写缓存区(A1,B1),然后从内存中读取另一个共享变量(A2,B2),最后才把自己写缓冲区中保存的脏数据刷新到内存中(A3,B3).当以这种时序执行时,程序就可以得到x=y=0的结果.

  这里的关键是,由于写缓冲区仅对自己的处理器可见,它会导致处理器执行内存操作的顺序可能会与内存实际的操作执行顺序不一致.由于现代的处理器都会使用写缓冲区,因此现代的处理器都会允许写-读操作的重排序.

  下面是常见处理器允许的重排序类型的列表:

  

  从表单元格的"n"表示处理器不允许两个操作重排序,"Y"表示允许重排序.

  从上表我们可以看出:常见的处理器都允许Store-load重排序;常见的处理器都不允许对存在数据依赖的操作做重排序.spare-TSO和x86拥有相对较强的处理器内存模型,他们仅允许对写-读操作做重排序.

  为了保证内存可见性,java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序.JMM把内存屏障指令分为下列四类:

  

  

  StoreLoad Barriers是一个"全能型"的屏障,它同时具有其他三个屏障的效果.现代的多处理器大都支持该屏障(其他类型的屏障不一定被所有处理器支持).执行该屏障开销会很昂贵,因为当前处理器通常要把写缓冲区的数据全部刷新到内存中(buffer fully flush).

重排序之数据依赖性

  如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性.数据依赖性分下列三种类型:

  

  上面三种情况,只要重排序两个操作的执行顺序,程序的执行结果将会被改变.

  编译器和处理器可能会对操作做重排序.编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序.

  注意:这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑.

happens-before

  从JDK1.5开始,java使用新的JSR-133内存模型.JSR-133使用happens-before的概念来阐述操作之间的内存可见性.在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这个两个操作之间必须要存在happens-before关系.这里提到的两个操作即可以是在一个线程之内,也可以是在不同线程之间.

  与程序员密切相关的happens-before规则如下:

  程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作.

  监视器锁规则:对一个监视器的解锁,happens-before于随后对这个监视器的加锁.

  volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读.

  传递性:如果A happens-before B,且 B happens-before C,那么A happens-before C.

  注意:两个操作之具有Happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行.happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前.

as-if-serial语义

  as-if-serial语义的意思指:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变.编译器,runtime和处理器都必须遵守as-if-serial语义.

  为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这个重排序会改变执行结果.但是,如果操作之间不存在数据依赖性关系,这些操作就可能被编译器和处理器重排序.

  as-if-serial语义把单线程程序保护了起来,遵守了as-if-serial语义的编译器,runtime和处理器共同为编写单线程程序的程序员创建了一个幻觉:单线程程序是按程序的顺序来执行的.as-if-serial语义使单线程程序员无须担心重排序干扰他们,也无需担心内存可见性问题.

原文地址:https://www.cnblogs.com/maybechen/p/8716653.html

时间: 2024-08-03 22:55:39

java内存模型之重排序的相关文章

《java并发编程实战》读书笔记13--Java内存模型,重排序,Happens-Before,

第16章 Java内存模型 终于看到这本书的最后一章了,嘿嘿,以后把这本书的英文版再翻翻.这本书中尽可能回避了java内存模型(JMM)的底层细节,而将重点放在一些高层设计问题,例如安全发布,同步策略等.它们的安全性都来自于JMM.本章将介绍Java内存模型的底层需求以及所提供的保证. 16.1 什么是内存模型,为什么需要它 16.1.1 平台的内存模型 在共享内存的多处理体系架构中,每个处理器都拥有自己的缓存,并且定期地与住内存进行协调.在不同的处理器架构中提供了不同级别的缓存一致性.要想确保

Java内存模型分析

在学习Java内存模型之前,先了解一下线程通信机制. 1.线程通信机制 在并发编程中,线程之间相互交换信息就是线程通信.目前有两种机制:内存共享与消息传递. 1.1.共享内存 Java采用的就是共享内存,本次学习的主要内容就是这个内存模型. 内存共享方式必须通过锁或者CAS技术来获取或者修改共享的变量,看起来比较简单,但是锁的使用难度比较大,业务复杂的话还有可能发生死锁. 1.2.消息传递 Actor模型即是一个异步的.非阻塞的消息传递机制.Akka是对于Java的Actor模型库,用于构建高并

<java并发编程的艺术>读书笔记-第三章java内存模型(一)

一概述 本文属于<java并发编程的艺术>读书笔记系列,继续第三章java内存模型. 二重排序 2.1数据依赖性 如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性.数据依赖分下列三种类型: 名称 代码示例 说明 写后读 a = 1;b = a; 写一个变量之后,再读这个位置. 写后写 a = 1;a = 2; 写一个变量之后,再写这个变量. 读后写 a = b;b = 1; 读一个变量之后,再写这个变量. 上面三种情况,只要重排序两个操作的执行顺序,

Java内存模型(二)——重排序

一.重排序 重排序是指为了提高程序的执行效率,编译器和处理器常常会对语句的执行顺序或者指令的执行顺序进行重排. 编译器优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序. 指令级并行的重排序:现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行.如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序. 内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序

深入理解Java内存模型(四)——volatile

volatile的特性 当我们声明共享变量为volatile后,对这个变量的读/写将会很特别.理解volatile特性的一个好方法是:把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步.下面我们通过具体的示例来说明,请看下面的示例代码: class VolatileFeaturesExample { //使用volatile声明64位的long型变量 volatile long vl = 0L; public void set(long l) { vl = l;

深入理解Java内存模型(1 ) -- 基础(转载)

原文地址:http://www.infoq.com/cn/articles/java-memory-model-1 并发编程模型的分类 在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执行的活动实体).通信是指线程之间以何种机制来交换信息.在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递. 在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信.在消息传递的并发模型里,线程之间没有公共状态,线

volotile关键字的内存可见性及重排序

在理解volotile关键字的作用之前,先粗略解释下内存可见性与指令重排序. 1. 内存可见性 Java内存模型规定,对于多个线程共享的变量,存储在主内存当中,每个线程都有自己独立的工作内存,并且线程只能访问自己的工作内存,不可以访问其它线程的工作内存.工作内存中保存了主内存中共享变量的副本,线程要操作这些共享变量,只能通过操作工作内存中的副本来实现,操作完毕之后再同步回到主内存当中,其JVM内存模型大致如下图. 而JAVA内存模型规定工作内存与主内存之间的交互协议,其中包括8种原子操作: 1)

(第三章)Java内存模型(中)

一.volatile的内存语义 1.1 volatile的特性 理解volatile特性的一个好办法是把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步.下面通过具体的示例来说明,示例代码如下: class VolatileFeaturesExample { volatile Long vl = 0L; //使用volatile声明64位的Long型变量 public void set(Long l) { vl = l; //单个volatile变量的写 } p

java内存模型-volatile

volatile 的特性 当我们声明共享变量为 volatile 后,对这个变量的读/写将会很特别.理解 volatile 特性的一个好方法是:把对 volatile 变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步.下面我们通过具体的示例来说明,请看下面的示例代码: class VolatileFeaturesExample { //使用volatile声明64位的long型变量 volatile long vl = 0L; public void set(long l) {