多线程中的指令重排序

指令重排序

Java内存模型里面允许编译器和处理器对指令进行重排序以提高运行效率,并且只会对不存在数据依赖的指令进行重排序。例如像

int a  = 1;
int b = a+1;
//这里的就不会重排序,因为两条指令之间存在数据依赖
int a = 1;
int b = 1;
int c = a+b;
//这里的指令就满足重排序的条件 第一条第二条之间不存在数据依赖, 就可以重排序
//但是不是说满足条件就一定会重排序,具体还是看编译器分析重排是否会提高性能 在这里不会提高性能,因此应当不会进行重排

指令重排序的好处

指令重排序有利于提高性能,可以看这个代码。

//section1
int a = 1;
int b = 1;
a=a+1;
b=b+1;
//section2
int a = 1;
a=a+1;
int b = 1;
b=b+1;

第一部分的代码可能就不会有部分2的性能高,因为a可以直接从寄存器中取,不需要反复的拿a。定义完a之后马上就可以进行add操作。性能理论上来说会好于第一部分的代码

指令重排的问题

单线程

在单线程中,不管怎么重排序,都不会出现问题,因为重排后也能保证结果的一致性。就像上面那部分的这个代码

int a = 1; //1
int b = 1;//2
int c = a+b; //3

因为数据依赖的限制,3对1,2都有数据依赖,所以可以保证一定会在1,2的后面,但是1和2谁先执行则不一定。但是在单线程的情况下,他们谁先执行都可以保证c=2,结果不会发生变化

多线程

但是在多线程中就可能出现问题,例如下面这段代码

  class ReadThread extends Thread {

    @Override
    public void run() {

        while (!Thread.currentThread().isInterrupted()){
            if (flag){ //1
                System.out.println(num+num); //2
            }
            System.out.println("读线程。。。。。。");
        }
    }

    public static  int num = 0;
    public static  boolean flag = false;

     static class WriteTheed extends  Thread{
         @Override
         public void run() {
             num = 2; //3
             flag = true; //4
         }
     }

      public static void main(String[] args) throws InterruptedException {
          ReadThread readThread = new ReadThread();
          readThread.start();

          WriteTheed wirteThred = new WriteTheed();
          wirteThred.start();

          Thread.sleep(1000);
          readThread.interrupt();
          System.out.println("主函数退出");

      }
}

在这段代码里面,如果不存在指令重排序的话,正常逻辑应当是这样。读线程读到1,发现flag为false,因此打印 读线程....,然后读线程丢失cpu执行权。于是到了写线程,将num改为2,flag改为true,于是,读线程发现flag为true,执行num+num操作,打印4

但是在多线程情况下,指令重排序的情况下,由于1,2,3,4都不存在数据依赖,鬼才知道他们会排成什么样子,就可能出现这种情况,排成,4,1,2,3,读线程读到1,发现flag为false,因此打印 读线程....,然后读线程丢失cpu执行权。于是到了写线程,由于重排成了4,1,2,3, flag改为true,然后丢失了cpu执行权,执行1,发现flag为true,执行2,此时的num还为0,执行num+num,打印0,再执行3.最后结果为0.

这就是指令重排序带来的问题,结果不一致。要解决这个问题也很简单,只需要将flag声明为volatile,然后每个线程读取的时候都会从主存读值,并且声明为volatile还可以通过内存屏障解决指令重排的问题,可以保证volatile写操作之前的那条指令,不会被排到volatile写之后。读volatile的时候,也可以确保读volatile之后的操作,不会被排到volatile读值钱。通俗来说就是这样

volatile int flag = false;
int num = 0
//1  ---- 线程1
if(flag){
    ......
}

num =2;//2
flag = ture;//3
//在上面这段代码中,由于flag被volatile修饰 因此可以保证 2一定是在1之后,3一定是在2之后
    

关于这个问题是怎么实现的,会在之后的volatile关键字中讲到。

posted @ 2020-05-10 17:15  穿黑风衣的牛奶  阅读(866)  评论(0编辑  收藏  举报