Java 内存模型允许编译器和处理器对指令重排序以提高运行性能,并且只会对不存在数据依赖性的指令重排序。在单线程下重排序可以保证最终执行的结果与程序顺序执行的结果一致,但是在多线程下就会存在问题。
下面看一个例子。
int a = 1; (1)
int b = 2; (2)
int c= a + b; (3)
在如上代码中,变量 c 的值依赖 a 和 b 的值,所以重排序后能够保证(3)的操作在(2)(1)之后,但是(1)(2)谁先执行就不一定了,这在单线程下不会存在问题,因为并不影响最终结果。
下面看一个多线程的例子。
public static class ReadThread extends Thread {
public void run() {
while(! Thread.currentThread().isInterrupted()){
if(ready){//(1)
System.out.println(num+num); //(2)
}
System.out.println("read thread....");
}
}
}
public static class Writethread extends Thread {
public void run() {
num = 2; //(3)
ready = true; //(4)
System.out.println("writeThread set over...");
}
}
private static int num =0;
private static boolean ready = false;
public static void main(String[] args) throws InterruptedException {
ReadThread rt = new ReadThread();
rt.start();
Writethread wt = new Writethread();
wt.start();
Thread.sleep(10);
rt.interrupt();
System.out.println("main exit");
}
首先这段代码里面的变量没有被声明为 volatile 的,也没有使用任何同步措施,所以在多线程下存在共享变量内存可见性问题。这里先不谈内存可见性问题,因为通过把变量声明为 volatile 的本身就可以避免指令重排序问题。
这里先看看指令重排序会造成什么影响,如上代码在不考虑内存可见性问题的情况下一定会输出 4?答案是不一定,由于代码(1)(2)(3)(4)之间不存在依赖关系,所以写线程的代码(3)(4)可能被重排序为先执行(4)再执行(3),那么执行(4)后,读线程可能已经执行了(1)操作,并且在(3)执行前开始执行(2)操作,这时候输出结果为 0 而不是 4。
重排序在多线程下会导致非预期的程序执行结果,而使用 volatile 修饰 ready 就可以避免重排序和内存可见性问题。
写 volatile 变量时,可以确保 volatile 写之前的操作不会被编译器重排序到 volatile 写之后。读 volatile 变量时,可以确保 volatile 读之后的操作不会被编译器重排序到 volatile 读之前。