JVM会在不影响正确性的前提下调整语句(指令)的顺序。
例如下代码:
static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...;
j = ...;
//可以看到,至于是先执行 i 还是 先执行 j ,对最终的结果不会产生影响。所以,上面代码真正执行时,既可以是
i = ...;
j = ...;
//也可以是
j = ...;
i = ...;
这种特性被称为指令重排,多线程的某些情况下指令重排会影响正确性。
1、为什么会有指令重排这项优化呢?
一个cpu执行指令是逐条执行的,但是如果我们把指令在进行划分,比如划分为:取指令 - 指令译码 - 执行指令 - 内存访问 - 数据 写回
五个阶段,那么Cpu实际执行过程是这样的
每个CPU中有不同的执行单元(位移单元、运算单元等),对应指令执行的不同步骤,在并行指令集引入之前,一个CPU只能同时执行一条指令,所以CPU中各个单元同一时间只有一个单元在工作。
并行指令集引入之后,同一时间CPU的各个单元可以同时运行,实现了真正意义上的指令级的并行操作。这意味着CPU同一时刻能够执行多条指令。
虽然CPU能够并行执行指令了,但是,有一些指令需要依赖前面指令的执行结果,所以不能并行执行,这时候就可以通过指令重排优化来实现CPU并行指令的目的,例如:
重新排序以后就af就可以并行执行。
2、CPU如何实现指令并行执行。
在研究指令重排是如何影响并发正确性之前,先额外看一下cpu如何实现指令并发执行
答案是流水线作业!
现代 CPU 支持多级指令流水线,例如支持同时执行 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回
的处理 器,就可以称之为五级指令流水线。这时 CPU 可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一 条执行时间最长的复杂指令),IPC = 1(IPC是每个时钟周期能运行的指令数),本质上,流水线技术并不能缩短单条指令的执行时间,但它变相地提高了 指令地吞吐率。
3、指令重排是如何影响并发正确性的
如下代码可能的执行结果 有:4,1和0
int num = 0;
boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
// 线程2 执行此方法
public void actor2(I_Result r) {
num = 2;
ready = true;
}
4和1恒容易理解,0就 有一些不可思议。而结果0出现的原因就是对actor2方法中的两行代码进行了重排序,交换了他们的执行次序。
但是在单线程的情形下,两个方法顺次执行,永远不会出现0这个结果。
4、如何解决多线程下的指令重排带来的问题
使用volatile的内存屏障功能。
使用volatile修饰的变量,在读或写之后会形成内存读写屏障的效果。
(1)写屏障
对volatile修饰的变量进行写操作之后(该操作被称为写屏障),该变量之前的代码执行结果会被写入到主存。同时会保证,写屏障之前的代码不会被重排序到写屏障之后。
(2)读屏障
对volatile修饰的变量进行读操作之后(该操作被称为读屏障),该变量之后的代码读取共享变量时会直接从主存中读取。同时,保证读屏障之后的代码不会被重排序到读屏障之前。
通过读写屏障也就解释了为什么volatile能够保证可见性和有序性:
- 可见性 :无论在哪个线程金星了修改都会立刻同步主存,同时从主存读。
- 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
- 而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
- 有序性
- 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
- 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
所以解决上述问题只需要在ready变量前用volatile修饰即可:这样就保证ready=true
(写屏障)之前的代码(num=2)不会被重排序ready=true
(写屏障)之后。
int num = 0;
volatile boolean ready = false;
// 线程1 执行此方法
public void actor1(I_Result r) {
if(ready) {
r.r1 = num + num;
} else {
r.r1 = 1;
}
}
// 线程2 执行此方法
public void actor2(I_Result r) {
num = 2;
ready = true;
}
注意:
- 同步代码块越短越好
- synchronized能解决有序、可见、原子性问题,但是不能防止重排序现象的发生。对于重排序这一点,如果共享变量的所有操作均在synchronized代码块内,是不会出现重排序导致的程序结果错误。