参考
https://www.jianshu.com/p/c6f190018db1
https://www.cnblogs.com/jackeason/p/11336306.html
https://blog.csdn.net/DingKG/article/details/103316423
指令重排
在计算机中,软件技术和硬件技术有一个共同的目标:在不改变程序执行结果的前提下,尽可能的开发并行度(性能)。
指令重排包括:
- 编译器的重排序
- CPU 指令集的重排序
- 内存系统的重排序
编译器 和 处理器 可能会对操作做重排序。编译器和处理器在重排序时,会遵守 数据依赖性,编译器和处理器不会改变存在数据依赖关系的两个操作的执行顺序。
注意,这里所说的数据依赖性仅针对 单处理器 中执行的指令序列和 单个线程 中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。
as-if-serial 语义
编译器,runtime 和处理器对(单线程)程序的指令重排都必须遵守 as-if-serial 语义。
happens- before 规则
happens-before 用来指定两个操作之间的执行顺序,由于这个两个操作可以在 一个线程 之内,也可以在 不同线程 之间。因此,JMM可以通过 happens-before关系来保证跨线程的内存可见性
- 如果一个操作“先于”另一个操作,那么第一操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前;
- 两个操作是happens-before的关系,也并不意味着JVM会按照这个关系去顺序执行,因为会存在重排序的可能,但是进行了重排序的执行结果,与此happens-before的关系顺序执行的结果一致的话,那就说明这个重排序是合法的(也就是JVM允许这样的重排序)。
happens- before 规则具体如下:
- 程序顺序规则:在一个线程内必须保证语义串行性,也就是按照代码顺序执行;
- 监视器(管程)锁规则:无论是单线程还是对线程环境,对于同一个锁来说,解锁操作必须是先于后一个加锁操作之前(如果A线程进行了加锁,还未进行解锁,那么B线程是不可能进行加锁操作的,只有等到A线程进行解锁操作之后,才能再进行加锁操作),而且前者线程解锁之后,对数据的操作对于后者加锁的线程是可见的;
- volatile 规则:volatile变量的写先于变量的读,保证了volatile变量的可见性,简而言之,volatile变量每次别线程访问时,都强迫从主内存中读该变量的值,而当变量的值被修改时,又会强迫将最新的值从工作内存刷回主内存中,任何时刻,不同线程总是能获取到该变量的最新值;
- 线程启动规则:线程的start方法先于此线程run方法中的所有操作(线程一定是执行start方法之后,才会执行真正的run方法逻辑),如果A线程在执行过程中,执行B线程的start方法,那么在A线程执行过程中到B线程start这一段区域中对共享变量的修改,对线程B是可见的;
- 线程终止规则:线程run方法中的执行操作一定是先于此线程的join方法的,如果B线程修改了共享变量的值,那么在B线程执行join方法之后,主线程一定对此共享变量是可见的;
- 线程中断规则:对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断;
- 对象终结规则:一个对象的初始化完成(构造函数执行完成)一定先于finalize方法(对象被回收时会调用,即垃圾回收时)之前执行,也就是现有对象才能进行对象回收操作。
重排序对多线程的影响
在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是 as-if-serial 语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。
编译器的重排序
编译源代码时,编译器依据对上下文的分析,对指令(字节码)进行重排序
CPU 指令集的重排序
CPU在执行过程中,动态分析依赖部件的效能,对指令做重排序优化。
内存系统的重排序
程序执行一段代码,写一个普通的共享变量,其可能先被写到缓冲区然后再被写到主内存,此时指令完成的时间就被推迟了。