1.重排序

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

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

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

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

 

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

  

java 指令重排序会导致数据库读取错误 指令重排序的种类_数据依赖

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

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

 

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

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

  

java 指令重排序会导致数据库读取错误 指令重排序的种类_runtime_02

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

  

java 指令重排序会导致数据库读取错误 指令重排序的种类_java_03

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

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

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

  

java 指令重排序会导致数据库读取错误 指令重排序的种类_java_04

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

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

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

  

java 指令重排序会导致数据库读取错误 指令重排序的种类_runtime_05

  

java 指令重排序会导致数据库读取错误 指令重排序的种类_runtime_06

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

 

重排序之数据依赖性

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

  

java 指令重排序会导致数据库读取错误 指令重排序的种类_数据依赖_07

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

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

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

 

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语义使单线程程序员无须担心重排序干扰他们,也无需担心内存可见性问题.