指令重排序,内存模型排序规则,内存屏障
1,指令重排序
大多数现代微处理器都会采用将指令乱序执行(out-of-order execution,简称OoOE或OOE)的方法,
在条件允许的情况下,直接运行当前有能力立即执行的后续指令,避开获取下一条指令所需数据时造成的等待。
通过乱序执行的技术,处理器可以大大提高执行效率。 除了处理器,常见的Java运行时环境的JIT编译器也会做指令重排序操作,即生成的机器指令与字节码指令顺序不一致。
2,as-if-serial语义
As-if-serial语义的意思是,所有的动作(Action)都可以为了优化而被重排序,但是必须保证它们重排序后的结果和程序代码本身的应有结果是一致的。
Java编译器、运行时和处理器都会保证单线程下的as-if-serial语义。
ps:即指令好像是连续的,是对这种执行效果特性的一个说法。
为了保证这一语义,重排序不会发生在有数据依赖的操作之中。
3,内存访问重排序与内存可见性
计算机系统中,为了尽可能地避免处理器访问主内存的时间开销,处理器大多会利用缓存(cache)以提高性能。
即缓存中的数据与主内存的数据并不是实时同步的,各CPU(或CPU核心)间缓存的数据也不是实时同步的。
这导致在同一个时间点,各CPU所看到同一内存地址的数据的值可能是不一致的。
从程序的视角来看,就是在同一个时间点,各个线程所看到的共享变量的值可能是不一致的。
有的观点会将这种现象也视为重排序的一种,命名为“内存系统重排序”。
因为这种内存可见性问题造成的结果就好像是内存访问指令发生了重排序一样。
(执行了却不知道执行了和以为执行了却重排序没有执行造成相同效果)
4,内存访问重排序与Java内存模型
Java的目标是成为一门平台无关性的语言,即Write once, run anywhere. 但是不同硬件环境下指令重排序的规则不尽相同。 例如,x86下运行正常的Java程序在IA64下就可能得到非预期的运行结果。 为此,JSR-1337制定了Java内存模型(Java Memory Model, JMM),旨在提供一个统一的可参考的规范,屏蔽平台差异性。 从Java 5开始,Java内存模型成为Java语言规范的一部分。
根据Java内存模型中的规定,可以总结出以下几条happens-before规则。
(ps:内存模型即通过运行环境把一些可见性和重排序问题统一成一个标准描述)
Happens-before的前后两个操作不会被重排序且后者对前者的内存可见。
程序次序法则: 线程中的每个动作A都happens-before于该线程中的每一个动作B,其中,在程序中,所有的动作B都能出现在A之后。 监视器锁法则: 对一个监视器锁的解锁 happens-before于每一个后续对同一监视器锁的加锁。 volatile变量法则:对volatile域的写入操作happens-before于每一个后续对同一个域的读写操作。 线程启动法则: 在一个线程里,对Thread.start的调用会happens-before于每个启动线程的动作。 线程终结法则:线程中的任何动作都happens-before于其他线程检测到这个线程已经终结、或者从Thread.join调用中成功返回,或Thread.isAlive返回false。 中断法则: 一个线程调用另一个线程的interrupt happens-before于被中断的线程发现中断。 终结法则: 一个对象的构造函数的结束happens-before于这个对象finalizer的开始。 传递性: 如果A happens-before于B,且B happens-before于C,则A happens-before于C
Happens-before关系只是对Java内存模型的一种近似性的描述,它并不够严谨,但便于日常程序开发参考使用,
关于更严谨的Java内存模型的定义和描述,请阅读JSR-133原文或Java语言规范章节17.4。
除此之外,Java内存模型对volatile和final的语义做了扩展。
对volatile语义的扩展保证了volatile变量在一些情况下不会重排序,volatile的64位变量double和long的读取和赋值操作都是原子的。
对final语义的扩展保证一个对象的构建方法结束前,所有final成员变量都必须完成初始化(前提是没有this引用溢出)。
(ps:没有理解final的意思)
Java内存模型关于重排序的规定,总结后如下表所示。(ps:下表没看懂)
5,内存屏障
内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。
Java编译器也会根据内存屏障的规则禁止重排序。
内存屏障可以被分为以下几种类型:
LoadLoad 屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
LoadStore 屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad 屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。
它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
有的处理器的重排序规则较严,无需内存屏障也能很好的工作,Java编译器会在这种情况下不放置内存屏障。
为了实现上一章中讨论的JSR-133的规定,Java编译器会这样使用内存屏障。(ps:下表没看懂)