欢迎大家搜索“小猴子的技术笔记”关注我的公众号,有问题可以及时和我交流。
我们在编写程序的时候有一个编写代码的顺序,那么计算机执行的时候就是按照我们编写代码的顺序来执行的吗?答案是:不一定。如果两个代码之间没有依赖关系的话,那么编译器和处理器常常会对我们的编码指令重排序。重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段,我们编写一个Java代码从源代码到最后的执行顺序如下:
源代码:也就是我们用开发工具写的代码。
编译器优化重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
指令级并行重排序:现代处理器采用了指令级并行技术来将多条指令重叠执行。如果数据不存在依赖,处理器就可以改变语句对应机器指令的执行顺序。
内存系统重排序:当代处理器使用写缓冲区来临时保存向内存写入的数据,这使得加载和存储操作看上去可能是在乱序执行。我们来看下面这个例子:
假设有处理器A和处理器B两个处理器,a和b的初始化状态为0 。在处理器A中执行下面代码(均为伪代码):
a=1;
x=b;
在处理器B中执行下面代码:
b=2;
y=a;
处理器允许执行后得到的结果是x=y=0。来看一下处理器和内存的交互图:
因为现代处理器都会使用写缓存,因此现在处理器都会允许对写-读的操作进行重排序。
写缓冲区的作用:因为处理器和内存的处理速度不是一个量级的,因此避免由于处理器停顿下来向内存写入数据而产生延迟,所以每个处理器都有一个仅仅对自己处理器可见的写缓冲区。现代处理器会通过批处理的方式刷新写缓冲区,以及合并写缓冲区中对同一个内存地址的多次写,减少对数据总线的调用。
介绍完了重排序之后,我们需要知道在单核处理器中,如果两个变量存在了数据依赖,编译器和处理器是不会改变存在数据依赖关系的两个操作的执行顺序的。那么重排序对多线程有什么影响呢?来看看下面的这个例子:
public class ReorderExample {
int a = 0;
boolean flag = false;
public void writer() {
// 操作1
a = 1;
// 操作2
flag = true;
}
public void reader() {
// 操作3
if (flag) {
// 操作4
int i = a * a;
System.out.println(i);
}
}
}
如果A线程先执行“writer()”方法,B线程接着执行“reader()”方法,那么线程B在执行的时候能否看到线程A对共享变量a的写入呢?
答案是不一定能看到,因为操作1和操作2没有数据依赖关系,所以编译器和处理器可以对这两个操作进行重排序。假定操作1和操作2进行了重排序,那么线程B在执行的时候得到的结果就有可能是i=0。
在操作3和操作4先进行了一个判断在计算,它们之间存在控制依赖关系。当代码中存在控制依赖性时,会影响指令序列执行的并行度。线程B处理器可以提前读取并计算“a*a”,然后把计算结果临时保存到一个名为重排序缓存(Reorder Buffer,ROB)的硬件缓存中。当操作3的条件判断为真的时候,就把该计算结果写入到变量i中。
由此可以明白,如果多线程的话,重排序是会影响多线程的执行结果的