- 指令重排基础概念
- 在现代处理器和编译器为了提高程序执行效率,会对指令进行优化,其中一种优化方式就是指令重排序。在单线程环境下,指令重排序不会影响最终执行结果,因为处理器和编译器会保证重排序后的执行结果与按照代码顺序执行的结果一致。但在多线程环境下,指令重排序可能会导致程序出现意外的行为。
- 例如,处理器可能会将一些不依赖于其他指令结果的指令提前执行,以充分利用处理器资源。编译器也可能对代码进行优化,改变指令的顺序。
volatile
禁止指令重排原理volatile
关键字具有禁止指令重排序的语义。Java内存模型(JMM)规定,对volatile
变量的写操作,先行发生于后续对这个volatile
变量的读操作。这意味着,在volatile
变量的写操作之前的所有操作,都必须在volatile
变量的写操作之前完成;而volatile
变量的读操作,必须在其之后的所有操作之前完成。这种规则确保了volatile
变量相关的操作顺序与代码顺序一致,从而避免了指令重排序带来的问题。
- 代码示例及执行顺序分析
- 示例1:单例模式中的指令重排问题与
volatile
作用
- 示例1:单例模式中的指令重排问题与
public class Singleton {// 未使用volatile时可能会出现指令重排问题private static Singleton instance; private Singleton() {}public static Singleton getInstance() {if (instance == null) { // 第一次检查synchronized (Singleton.class) {if (instance == null) { // 第二次检查// 这里可能发生指令重排instance = new Singleton(); }}}return instance;}
}
在上述代码中,instance = new Singleton();
这行代码实际上包含了三个步骤:
-
- 分配内存空间给
Singleton
对象。
- 分配内存空间给
-
- 初始化
Singleton
对象。
- 初始化
-
- 将
instance
指向分配的内存空间。
- 将
在没有 volatile
修饰的情况下,编译器和处理器可能会对这三个步骤进行重排序,比如先执行步骤1和3,然后再执行步骤2。假设线程A执行 getInstance()
方法,在步骤1和3执行后,但步骤2还未执行时,线程B进入 getInstance()
方法,此时 instance
已经非空(因为已经指向了分配的内存空间),线程B会直接返回 instance
,但此时 instance
还未初始化完成,这就会导致程序出错。
使用 volatile
修饰 instance
后:
public class Singleton {// 使用volatile防止指令重排private static volatile Singleton instance; private Singleton() {}public static Singleton getInstance() {if (instance == null) { // 第一次检查synchronized (Singleton.class) {if (instance == null) { // 第二次检查instance = new Singleton(); }}}return instance;}
}
volatile
关键字保证了这三个步骤不会被重排序,一定是按照1 -> 2 -> 3的顺序执行,从而确保了 instance
在被其他线程获取时已经完全初始化。
- 示例2:简单变量操作中的指令重排与
volatile
public class VolatileReorderingExample {private static int a = 0;private static volatile boolean flag = false;public static void main(String[] args) {Thread thread1 = new Thread(() -> {a = 1; // 语句1flag = true; // 语句2});Thread thread2 = new Thread(() -> {while (!flag) {// 等待flag变为true}System.out.println(a); // 语句3});thread1.start();thread2.start();}
}
在上述代码中,如果 flag
没有被声明为 volatile
,由于指令重排,语句1和语句2的执行顺序可能会被改变,即先执行 flag = true;
然后再执行 a = 1;
。这样当线程2执行到 System.out.println(a);
时,a
可能还没有被赋值为1,输出结果可能为0。
而当 flag
被声明为 volatile
后,JMM保证了语句1一定在语句2之前执行,并且语句1的结果对线程2可见。所以当线程2执行 System.out.println(a);
时,a
一定已经被赋值为1,输出结果为1。
总结来说,volatile
通过JMM的规则,限制了编译器和处理器对 volatile
变量相关指令的重排序,确保了多线程环境下程序的执行顺序符合预期,避免了因指令重排导致的错误。