关于java:聊聊多线程中的指令重排序

什么是指令重排序?

Java内存模型容许编译器和处理器对指令进行重排序来晋升运行性能,当然只会对那些不存在数据依赖的指令间进行重排序,不然的话会失去谬误执行后果。然而,这种保障只对单线程无效,多线程环境下并不能保障这点,这就导致了多线程环境下,重排序后运行的后果并不是咱们所预期的那样,这就是指令重排序。

上面举个栗子具体阐明:

先来看个失常不会出问题的栗子:

public int add() {
   int x = 1; //(1)
   int y = 2; //(2)
   int z = x + y; //(3)
   return z;
}

下面的代码中,(1)和(2)没有依赖关系,它们之间能够重排序,(3)依赖了(1)和(2),所以(3)不会被重排序到(1)或(2)之前,这个栗子中,(1)(2)即便产生了重排序,也不会影响程序运行后果。

再来看个可能会呈现问题的栗子:

public class InstructionReorderDemo {
    static int num = 0;
    static boolean ready = false;
    public static void main(String[] args) throws InterruptedException {
        Thread readThread = new Thread(() -> {
            if (ready) { //(1)
                System.out.println(num + num); //(2)
            }
        });
        Thread writeThread = new Thread(() -> {
               num = 2; //(3)
               ready = true; //(4)
        });
        readThread.start();
        writeThread.start();
        readThread.join();
        writeThread.join();
        System.out.println("main thread exit.");
    }
}

这个栗子中,因为(1)(2)(3)(4)之间都没有数据依赖,可能会有同学有疑难,(1)不是依赖了(4)吗,(2)不是依赖了(3)吗?别忘了,这4条指令放在单线程下能力保障这点,就是说上面的代码才会产生上述依赖:

int num;
boolean ready;
public void calculate {
    num = 2;
    ready = true;
    if (ready) {
      System.out.println(num + num);
    }
}

这里是多个线程别离访问共享变量ready和num,且它们都没有用volatile润饰,两个线程并发运行,编译器/处理器可能会对(3)(4)进行重排序,因为它们之间没有数据依赖。但这时,如果按如下工夫线运行,就会呈现运行后果是0,而不是4。

readThread writeThread
ready = true //(4)
if(ready) //(1)
num + num //(2)
num = 2 //(3)

如何防止指令重排序?

办法很简略,把上述程序中的ready变量用volatile润饰即可。volatile关键字不仅保障了内存可见性,还有内存屏障的作用,在JVM底层是用Lock前缀的指令实现的。它保障了在写volatile变量指令时,在它之前的指令不会被重排序到它之后,在这个栗子中就是num = 2不会在ready = true之后执行;在读volatile变量时,在它之后的指令不会被重排序到它之前,在这个栗子中就是num + num不会放在if (ready)之前执行。

参考资料:
《Java并发编程之美》

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理