一、指令重排序案例
指令重排序是CPU为了提高指令执行效率的一种优化方式,它会将指令按照一定规则重新排序。以下是一个指令重排序的经典案例:
public class DCLSingleton { private static volatile DCLSingleton instance; private DCLSingleton() {} public static DCLSingleton getInstance() { if (instance == null) { synchronized (DCLSingleton.class) { if (instance == null) { instance = new DCLSingleton(); } } } return instance; } }
在并发情况下,由于指令重排序的存在,可能会引发DCL(Double-Check Locking)失败的问题,使得单例模式失效。
二、指令重排序和JMM的关系
Java内存模型(Java Memory Model,JMM)是Java平台中各种编译器的内存模型的抽象。为了支持跨平台的Java编程,JMM规范了不同线程之间的内存可见性、数据原子性、指令重排序等问题,并提供了一套完整的内存屏障(Memory Barrier)语义规范。
在JMM的规范下,JVM对于指令重排序有以下保证
- 单线程中,按照指令出现的顺序执行。
- 多线程中,无法保证按照指令出现的顺序执行,但会保证在JMM规定下的“内存屏障”出现的时候,程序执行满足JMM的需求。
三、指令重排序什么意思
指令重排序是CPU为了提高指令执行效率而进行的一种优化方式。由于现代CPU都采用了流水线技术,当一条指令执行完后,下一条指令随即进入流水线被执行。而在执行过程中,如果出现了数据相关等问题,就需要停顿流水线等待数据,这样会降低CPU效率。为了尽可能避免出现这种情况,CPU引入了指令重排序技术,将原来的指令序列重新排序,尽可能保证流水线能够无延迟地执行指令。
四、指令重排序会有什么问题
虽然指令重排序能够提高系统的效率,但也可能引发一些隐患。最典型的例子是DCL。
在DCL的代码片段中,变量instance被声明为volatile,该关键字表示变量的修改对于其他线程都是可见的。但是如果在synchronized关键字后面的重排序过程中,创建对象的构造函数先执行,而在给instance赋值的过程中出现了重排序,则会引发DCL的失败。
五、指令重排单线程有问题
单线程在执行指令重排时,也存在隐患。以下这个例子可以说明这个问题:
public class Test { public static void main(String[] args) { int a = 0; boolean flag = false; a = 1; try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } if (a == 1 && !flag) { System.out.println("a==1"); } } }
在单线程中执行以上代码,有可能导致输出结果为空。这是因为JVM为了提高执行效率,可能会将if语句执行的代码片段与a=1这条语句进行重排序,导致程序逻辑错误。
六、代码示例
public class Test { public static void main(String[] args) { int a = 0; boolean flag = false; a = 1; try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } if (a == 1 && !flag) { System.out.println("a==1"); } } }
以上代码就体现了指令重排单线程的隐患。在多线程中,也要注意指令重排序可能引发的问题。