共计 1393 个字符,预计需要花费 4 分钟才能阅读完成。
什么是指令重排序?
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 并发编程之美》