目录
一、指令重排序(并发编程之美)
Java 内存模型允许编译器和处理器对指令重排序以提高运行性能,
并且只会对不存在数据依赖性的指令重排序。
在单线程下重排序可以保证最终执行的结果与程序顺序执行的结果一致,但是在多线程下就会存在问题。
下面看一个例子:
加载...
在如上代码中,变量 c 的值依赖a和b的值,所以重排序后能够保证(3)的操作在(2)(1)之后,
但是(1)(2)谁先执行就不一定了,这在单线程下不会存在问题,因为并不影响最终结果。
重排序在多线程下会导致非预期的程序执行结果,而使用volatile 修饰 ready 就可以避免重排序和内
存可见性问题。
写 volatile 变量时,可以确保 volatile 写之前的操作不会被编译器重排序到 volatile 写之后。
读 volatile 变量时,可以确保 volatile 读之后的操作不会被编译器重排序到 volatile 读之前。
二、指令重排序(自我整理)
1. 为什么需要指令重排序
其实说到底都是源于对性能的优化,CPU运行效率 相比缓存、内存、硬盘IO之间效率有着指数级的差
别,CPU作为系统的宝贵资源,那么如何更好的优化和利用这个资源就能提升整个计算机系统的性
能。
其实指令重排序就是一种来源于生活的优化思想,这种思想在生活中处处可见,就像平常咱们做菜,
咱们会选择在炒第一个菜的同时就在洗第二个菜了, 咱们会把熟得最慢的菜放到最开始(比如煲
汤),因为在等待这些菜熟的过程中(IO等待)咱们(CPU)还可以做其它事情,这就是一种时间上的
优化,在计算机领域也是一样,它也会根据指令的类别做一些优化,目的就是把CPU的资源利用起
来,这样就能就能提升整个计计算机的效率。
2. 什么是指令重排序
java语言规范规定JVM线程内部维持顺序化语义。
即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此
过程叫指令的重排序。
3. 指令重排序分类
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。
3.1. 编译器优化重排序
编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序
3.2. 指令级并行的重排序
现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。
如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
3.3. 内存系统的重排
由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
重排序流如下图所示:
从java源代码到最终实际执行的指令序列,会分别经历下面三种重排序:
上述的1属于编译器重排序,2和3属于处理器重排序。
这些重排序都可能会导致多线程程序出现内存可见性问题。
对于编译器,JMM的编译器重排序规则会禁止特定类型的编译器重排序
(不是所有的编译器重排序都要禁止)。
对于处理器重排序,JMM的处理器重排序规则会要求java编译器在生成指令序列时,插入特定类型的
内存屏障(memory barriers,intel称之为memory fence)指令,通过内存屏障指令来禁止特定类型
的处理器重排序(不是所有的处理器重排序都要禁止)。
JMM属于语言级的内存模型,它确保在不同的编译器和不同的处理器平台之上,
通过禁止特定类型的编译器重排序和处理器重排序,为程序员提供一致的内存可见性保证。
4. 指令重排序规范
4.1. as-if-serial 规范
单个线程中, 指令的重排 , 不能影响程序的执行结果 ;
可以重排的情况 : 对于下面代码 , 两条指令顺序颠倒 , 执行结果相同 , 可以进行指令重排 ;
int a = 0;
int b = 10;
不可以进行重排的情况 : 对于下面的代码 , 两条指令如果上下颠倒 , 结果不同 , 不可以进行指令重排 ;
int a = 0;
int b = a;
不管怎么重排序,程序的执行结果不能被改变,编译器,runtime 和处理器都必须遵守as-if-serial语
义。即编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会改变存在数据依赖关系
的两个操作的执行顺序。
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排
序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作可能被编译器和处理器重排
序。
as-if-serial语义把单线程程序保护了起来,遵守as-if-serial语义的编译器,runtime 和处理器共同为编
写单线程程序的程序员创建了一个
幻觉:单线程程序是按程序的顺序来执行的。
as-if-serial语义使单线程程序员无需担心重排序会干扰他们,也无需担心内存可见性问题。
4.2. happens-before 规范
先行发生原则 。
happens-before 先行发生原则 :A happens-before B,A 先于 B 发生 , 先 A 后 B ;
Java 虚拟机在编译时和运行时 , 会对 JVM 指令进行重排优化 , 很明显 , 指令重排会对线程并发产生影
响 ;为了保证并发编程的安全性,这里规定了一些场景下 , 禁止在这些场景中 使用 指令重排 ;
happens-before 先行发生原则 适用场景
程序次序原则
在程序内 , 按照代码书写的执行顺序 , 前面的代码先执行 , 后面的代码后执行 ; 时间上 靠前 的操作先
于时间上靠后 的操作执行。
管程锁规则
不论是单线程还是多线程 , 线程 A 解锁后 , 线程 B 获取该锁 , 可以看到线程 A 的操作结果 ;
解锁的操作先于加锁的操作 ; 线程 B 要加锁 , 必须等待线程 A 解锁完毕才可以 ;
volatile 规则
volatile 关键字修饰的变量 , 线程 A 对该变量的写操作 先于 线程 B 读取该变量的操作 ,
线程 A 对该变量的写操作的结果对于线程 B 一定可见 ;
线程启动规则
线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的
值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见。
线程终止规则
线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。
假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的
修改将对线程A可见。
传递性
happens-before 规则具有传递性 ;如果 A happens-before B 和 B happens-before C ,则 A happens-
before C ;
线程中断规则
对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过
Thread.interrupted()方法检测线程是否中断。
对象终结规则
对象的创建先于对象的终结,, 创建就是调用构造函数 , 终结就是调用finalize()方法
只要符合上述规则 , 不需要进行同步 , 就可以成立 ;
通过 " happens-before 先行发生原则 " 可以判定两个线程的操作 , 是否有发生冲突的可能 ;
5. 重排序对于流水线的意义
现代CPU几乎都采用流水线机制加快指令的处理速度,一般来说,一条指令需要若干个CPU时钟周期
处理,而通过流水线并行执行,可以在同等的时钟周期内执行若干条指令,具体做法简单地说就是把
指令分为不同的执行周期,例如读取、寻址、解析、执行等步骤,并放在不同的元件中处理,同时在
执行单元EU中,功能单元被分为不同的元件,例如加法元件、乘法元件、加载元件、存储元件等,可
以进一步实现不同的计算并行执行。流水线架构决定了指令应该被并行执行,而不是在顺序化模型中
所认为的那样。重排序有利于充分使用流水线,进而达到超标量的效果。