什么是指令重排序呢?为了更加直观地理解,老司机还是通过一个案例来说明
public class MemoryReorderingExample {private static int x=0,y=0:private static int a=0,b=0;public static void main(String[] args) throws InterruptedException {int i=0;while(true){i++;x=0;y=0;a=0;b=0;Thread t1=new Thread(()->{a=1;x=b;});Thread t2=new Thread(()->{b=1 y=a;});t1.start();t2.start();t1.join();t2.join();String result="第"+i+"次("+x+","+y+")";if(x==0&&y==0){System.out.println(result);break;}}}
}
上面这段程序的逻辑如下:
- 定义四个int类型的变量,初始化都为0。
- 定义两个线程t1、t2,t1 线程修改a和x的值,t2 线程修改 b 和 y 的值,分别启动两个线程
- 正常情况下,x和y的值,会根据 t1 和 t2 线程的执行情况来决定。
- 如果t1线程优先执行,那么得到的结果是x=0、y=1。
- 如果t2线程优先执行,那么得到的结果是x=1、y=0。
- 如果t1和t2线程同时执行,那么得到的结果是x=1、y=1。
我们来看一下运行结果:
第136161次(0,0)
大家看到这个结果是不是大吃一惊?在运行了13万次之后,竟然得到一个 x=0、y=0 的结果
其实这就是所谓的指令重排序问题,假设上面的代码通过指令重排序之后,变成下面这种结构
Thread t1=new Thread(()->{x=b; //指令重排序a=1;
});
Thread t2=new Thread(()->(y=a; //指令重排序b=1;
});
经过重排序之后,如果 t1 和 t2 线程同时运行,就会得到x=0、y=0的结果,这个结果从人的视角来看,就有点类似于t1线程中 a=1 的修改结果对 t2 线程不可见,同样 t2 线程中 b=1 的执结果对t1线程不可见。
什么是指令重排序
指令重排序是指编译器或 CPU 为了优化程序的执行性能而对指令进行重新排序的一种手段重排序会带来可见性问题,所以在多线程开发中必须要关注并规避重排序。
从源代码到最终运行的指令,会经过如下两个阶段的重排序
第一阶段,编译器重排序,就是在编译过程中,编译器根据上下文分析对指令进行重排序目的是减少CPU和内存的交互,重排序之后尽可能保证CPU从寄存器或缓存行中读取数据。在前面分析JIT优化中提到的循环表达式外提(Loop Expression Hoisting)就是编译器层面的重排序,从CPU层面来说,避免了处理器每次都去内存中加载stop,减少了处理器和内存的交互开销。
if(!stop){while(true){i++;}
}
第二阶段,处理器重排序,处理器重排序分为两个部分
- 并行指令集重排序,这是处理器优化的一种,处理器可以改变指令的执行顺序。
- 内存系统重排序,这是处理器引入Store Buffer 缓冲区延时写入产生的指今执行顺序不一致的问题,在后续内容中会详细说明。
为了帮助读者理解,笔者专门针对并行指令集的原理做一个简单的说明。
什么是并行指令集?在处理器内核中一般会有多个执行单元,比如算术逻辑单元、位移单元等。在引入并行指令集之前,CPU在每个时钟周期内只能执行单条指令,也就是说只有一个执行单元在工作,其他执行单元处于空闲状态;在引入并行指令集之后,CPU在一个时钟周期内可以同时分配多条指令在不同的执行单元中执行。
那么什么是并行指令集的重排序呢?如图所示,假设某一段程序有多条指令,不同指令的执行实现也不同。对于一条从内存中读取数据的指令,CPU 的某个执行单元在执行这条指令并等到返回结果之前,按照CPU 的执行速度来说它足够处理几百条其他指令,而 CPU为了提高执行效率,会根据单元电路的空闲状态和指令能否提前执行的情况进行分析,把那些指令地址顺序靠后的指令提前到读取内存指令之前完成。
实际上,这种优化的本质是通过提前执行其他可执行指令来填补 CPU的时间空隙,然后在结束时重新排序运算结果,从而实现指令顺序执行的运行结果。
讲到这里,大家先消化一下,思考一下,后面还有哪里不足的请指出来,我也好改正