jvm指令重排

JVM会在不影响正确性的前提下调整语句(指令)的顺序。

例如下代码:

static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...; 
j = ...; 
//可以看到,至于是先执行 i 还是 先执行 j ,对最终的结果不会产生影响。所以,上面代码真正执行时,既可以是
i = ...; 
j = ...; 
//也可以是
j = ...;    
i = ...; 

这种特性被称为指令重排,多线程的某些情况下指令重排会影响正确性。

1、为什么会有指令重排这项优化呢?

一个cpu执行指令是逐条执行的,但是如果我们把指令在进行划分,比如划分为:取指令 - 指令译码 - 执行指令 - 内存访问 - 数据 写回 五个阶段,那么Cpu实际执行过程是这样的

image-20220507092816313

每个CPU中有不同的执行单元(位移单元、运算单元等),对应指令执行的不同步骤,在并行指令集引入之前,一个CPU只能同时执行一条指令,所以CPU中各个单元同一时间只有一个单元在工作。

并行指令集引入之后,同一时间CPU的各个单元可以同时运行,实现了真正意义上的指令级的并行操作。这意味着CPU同一时刻能够执行多条指令。

虽然CPU能够并行执行指令了,但是,有一些指令需要依赖前面指令的执行结果,所以不能并行执行,这时候就可以通过指令重排优化来实现CPU并行指令的目的,例如:

image-20220507095836711

重新排序以后就af就可以并行执行。

2、CPU如何实现指令并行执行。

在研究指令重排是如何影响并发正确性之前,先额外看一下cpu如何实现指令并发执行

答案是流水线作业!

现代 CPU 支持多级指令流水线,例如支持同时执行 取指令 - 指令译码 - 执行指令 - 内存访问 - 数据写回 的处理 器,就可以称之为五级指令流水线。这时 CPU 可以在一个时钟周期内,同时运行五条指令的不同阶段(相当于一 条执行时间最长的复杂指令),IPC = 1(IPC是每个时钟周期能运行的指令数),本质上,流水线技术并不能缩短单条指令的执行时间,但它变相地提高了 指令地吞吐率。

image-20220507100810720

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; 
}

image-20220507110854143

注意:

  • 同步代码块越短越好

  • synchronized能解决有序、可见、原子性问题,但是不能防止重排序现象的发生。对于重排序这一点,如果共享变量的所有操作均在synchronized代码块内,是不会出现重排序导致的程序结果错误。

  • 8
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值