指令重排
谈到指令重排,首先来了解一下Java内存模型(JMM)。
JMM的关键技术点都是围绕多线程的原子性、可见性、有序性来创建的。
原子性(Atomicity)
原子性是指一个操做是不可中断的,即便是在多个线程一块儿执行的时候,一个操做一旦开始,就不会被其它线程干扰。
可见性(Visibility)
可见性是指当一个线程修改了某一个共享变量的值,其余线程是否可以当即知道这个修改。
对于串行程序来讲可见性问题是不存在的,由于在任何一个操做步骤中修改了某个变量,那么后续的步骤中,读取这个变量的值,必定是修改以后的。
可是在并行程序中,若是一个线程修改了某一个全局变量,那么其余线程未必能够立刻知道这个改动。(这里涉及到编译器优化重排和硬件优化,这里不重点讲述)
有序性(Ordering)
有序性是指在单线程环境中, 程序是按序依次执行的.
而在多线程环境中, 程序的执行可能由于指令重排而出现乱序。
` class OrderExample {
int a = 0;
boolean flag = false;安全
public void writer() { // 如下两句执行顺序可能会在指令重排等场景下发生变化 a = 1; flag = true; } public void reader() { if (flag) { int i = a + 1; …… } } }`
假设线程A首先执行write()方法,接着线程B执行reader()方法,若是发生指令重排,那个线程B在执行 int i = a + 1;时不必定能看见a已经被赋值为1了。多线程
指令1 IF ID EX MEN WB
指令2 IF ID EX MEN WB架构
指令的每一步都由不一样的硬件完成,假设每一步耗时1ms,执行完一条指令需耗时5ms,每条指令都按顺序执行,那两条指令则需10ms。
可是经过流水线在指令1刚执行完IF,执行IF的硬件立马就开始执行指令2的IF,这样指令2只须要等1ms,两个指令执行完只须要6ms,效率会有提高巨大!
因此经过流水线技术,可使得CPU高效执行,当流水线满载时,全部硬件都有序高效执行,可是一旦中断,全部硬件设备都会进入一个停顿期,再次满载
须要几个周期,所以性能损失会比较大,因此必须想办法尽可能不让流水线中断!
此时,指令重排的重要性就此体现出来。固然,指令重排只是减小中断的一种技术,实际上,在CPU设计中还会使用更多的软硬件技术来防止中断。app
如今来看一下代码 A=B+C 是怎么执行的
现有R1,R2,R3三个寄存器,
LW R1,B IF ID EX MEN WB(加载B到R1中)
LW R2,C IF ID EX MEN WB(加载C到R2中)
ADD R3,R2,R1 IF ID × EX MEN WB(R1,R2相加放到R3)
SW A,R3 IF ID x EX MEN WB(把R3 的值保存到变量A)
在ADD指令执行中有个x,表示中断、停顿,ADD为何要在这里停顿一下呢?由于这时C还没加载到R2中,只能等待,而这个等待使得后边的全部指令都会停顿一下。
这个停顿能够避免吗?固然是能够的,经过指令重排就能够实现,再看一下下面的例子:jvm
执行A=B+C;D=E-F;
经过将D=E-F执行的指令顺序提早,从而消除因等待加载完毕的时间。
一、LW Rb,B IF ID EX MEN WB
二、LW Rc,C IF ID EX MEN WB
三、LW Re,E IF ID EX MEN WB
四、ADD Ra,Rb,Rc IF ID EX MEN WB
五、LW Rf,F IF ID EX MEN WB
六、SW A,Ra IF ID EX MEN WB
七、SUB Rd,Re,Rf IF ID EX MEN WB
八、SW D,Rd IF ID EX MEN WB
在CPU硬件中断停顿等待的时候 能够加载别的数据,更加有效利用资源,节约时间。若是不指令重排则白白等待,效率较低。函数
编译器优化
主要指jvm层面的, 以下代码, 在jvm client模式很快就跳出了while循环, 而在server模式下运行, 永远不会中止
`/**oop
Created by Administrator on 2020/11/19
*/
public class VisibilityTest extends Thread {
private boolean stop;性能
public void run() {
int i = 0;
while (!stop) {
i++;
}
System.out.println("finish loop,i=" + i);
}优化
public void stopIt() {
stop = true;
}线程
public boolean getStop() {
return stop;
}
public static void main(String[] args) throws Exception {
VisibilityTest v = new VisibilityTest();
v.start();
Thread.sleep(1000);
v.stopIt();
Thread.sleep(2000);
System.out.println("finish main");
System.out.println(v.getStop());
}
}`
二者区别在于当jvm运行在-client模式的时候,使用的是一个代号为C1的轻量级编译器,而-server模式启动的虚拟机采用相对重量级,代号为C2的编译器. C2比C1编译器编译的相对完全,会致使程序启动慢, 但服务起来以后, 性能更高, 同时有可能带来可见性问题.
再来看两个从Java语言规范中摘取的例子, 也是涉及到编译器优化重排, 这里再也不作详细解释,可查询相关文档
例子1中有可能出现r2 = 2 而且 r1 = 1;
例子2中是r2, r5值由于都是=r1.x, 编译器会使用向前替换, 把r5指向到r2, 最终可能致使r2=r5=0, r4 = 3;
lfence指令读屏障(Load Barrier),做用是:
保证了lfence先后的Load指令的顺序,防止Load重排序
刷新Load Buffer
mfence指令全屏障(Full Barrier),做用是:
保证了mfence先后的Store和Load指令的顺序,防止Store和Load重排序
保证了mfence以后的Store指令全局可见以前,mfence以前的Store指令要先全局可见
JVM层级:8个hanppens-before原则 4个内存屏障 (LL LS SL SS)
Happen-Before先行发生规则
若是光靠sychronized和volatile来保证程序执行过程当中的原子性, 有序性, 可见性, 那么代码将会变得异常繁琐.
JMM提供了8个Happen-Before规则来约束数据之间是否存在竞争, 线程环境是否安全, 具体以下:
4个内存屏障 (LL LS SL SS)
LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操做要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操做执行前,保证Store1的写入操做对其它处理器可见。
LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操做被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续全部读取操做执行前,保证Store1的写入对全部处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。
as-if-serial
As-if-serial语义的意思是,全部的动做(Action)均可觉得了优化而被重排序,可是必须保证它们重排序后的结果和程序代码自己的应有结果是一致的。Java编译器、运行时和处理器都会保证单线程下的as-if-serial语义。