多线程常见陷阱之重排序

一.何为重排序?

  重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。

二.重排序是如何发生的?

  当一段代码中的其中几行数据不会出现数据依赖性的关系时,他就有可能被编译器和处理器为了提高编译器和处理器的并行度等原因而进行重排序。导致你原本可能想要的逻辑出现了本不能出现的bug。

  先说说数据依赖性是什么,然后我们再来看一个出现重排序报错的实例。

  (一)数据依赖性

    如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据的依赖性。例如:

double a=10;  //操作A
double b=20;  //操作B
double c=(a+b)*2;  //操作C

此时,由于操作A和操作C之间存在了写操作,且a是两个操作共同访问的变量,那么此时操作A和C他们就存在数据依赖性。那么操作A和C是不会出现重排序现象的。但是对于操作A和B,他们并没有访问同一个变量,于是在编译器和处理器进行优化时,他就有可能先进行操作B,再进行操作A,这样的现象就是重排序。

(二)因重排序而导致的原本不可能出现的bug

  在上诉的例子中,我们虽然产生了重排序,但是对于代码的结果来说,并没有任何问题,因为这是在单个线程中的例子。当牵扯到了多线程,那么就可能会出现意想不到的问题。例如:


class Example{
    int a = 1;
    boolean flag = false;
    public void writer(){
        a = 2; // 操作A
        flag = true; //操作B 
}
    public void reader(){
        if(flag){          // 操作C
            int i = a * a;  // 操作D
             ......
    }
} 
}

  当有两个线程X和Y分别执行writer()和reader()方法时,由于操作A和操作B之间并没有数据依赖性,因此他们可能会被重排序。这时候,如果出现线程X先执行了操作B,还未执行操作A时,线程Y读到了flag==true的结果,那么操作C就会执行,最后i的值就会等于1。而我们想要的逻辑是希望当a=2时才进入操作C,i的预期结果应该是4。此时如果未考虑多线程就会出现了一个无法理解的重排序的bug了。(有人会觉得操作C和操作D也可能会出现重排序问题。但是实际上并不会,因为当操作C和D出现重排序时,编译器和处理器会进行猜测执行来对类似if之类的判断进行出来,即使先执行了操作D,那也不会赋值给i,而是会先计算好a*a的结果,然后临时保存到一个名为重排序缓冲的硬件缓存中。只有操作C的条件满足了,才会将缓存的计算结果赋值给i)。

三.那么多线程中如何解决重排序问题呢?

  1.使用同步方法或者同步代码块synchronized执行多线程的代码就能避免了。

  2.当某个字段是多个线程共同持有的共享变量,此时,我们可以使用volatile关键字来修饰他,例如上述代码我们只要对a加上volatile关键字即可解决问题,即

class Example{
    volatile int a = 1;
    boolean flag = false;
    public void writer(){
        a = 2; // 操作A
        flag = true; //操作B 
}
    public void reader(){
        if(flag){          // 操作C
            int i = a * a;  // 操作D
             ......
    }
} 
}

因为volatile关键字修饰的变量会禁止他的方法体进行重排序(volatile的有序性)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值