顺序执行性内存模型: 是一个理论模型,人头脑中认为的正确的内存模型。 顺序一致性: 如果程序是正确同步的,程序的执行将具有顺序一致性;也可以说成正确同步程序的执行结果与顺序一致性内存模型的执行结果相同。(同步是广义的同步,包括同步原语 synchronized、volatile、final 的正确使用)
优势:编译器和处理器为了优化程序性能,可以对指令序列进行重新排序。
重排序不影响单线程程序的执行结果:因为重排序必须遵守数据依赖性,只有相互之间无依赖的的指令才可以重排序,而对无依赖的指令进行重排序不影响单线程程序的执行结果。
重排序会影响多线程程序的执行结果,包括:
编译期优化的重排序:编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。指令级并行的重排序:如果不存在数据依赖性,处理器可以改变语句对应的机器指令的执行顺序。下面举例说明:
上述代码中,假设线程A 首先执行 write() 而线程B 后执行 read(),则可能的时序为:
最后得到结果为:
a = 2 flag = true
而由于重排序,1 和 2 可能会进行互换,得到的时序为:
最后得到结果为:
a = 1 flag = true
可以看到重排序导致了两种不同的执行结果;可以使用同步解决,如下:
class RecorderExample{ int a = 0; boolean flag = false; public synchronized void write(){ a = 1; flag = true; } public synchronized void read(){ if(!flag){ a = 2; } }}同样,假设线程A 首先执行 write()和线程B 后执行 read(),对比一下 JMM 中的执行时序和顺序一致性内存模型中的执行时序:
因为加了锁同步,所以同步块内的重排序并不影响多线程的执行结果。
JMM的基本方针是在不改变正确同步程序的执行结果的情况下,尽可能地利用重排序进行性能优化。因此同步块内临界区的重排序是允许的。 针对未同步程序(包括单线程和多线程),顺序一致性内存模型能保证其顺序一致性,JMM 不保证其顺序一致性。
以双重检查锁定为例:
public class Singleton { PRivate static Singleton instance = null; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; }}对象的初始化可以分解为以下伪代码:
memory = allocate(); // 1:分配对象的内存空间ctorInstance(memory); // 2:初始化对象instance = memory; // 3:设置 instance 指向内存空间而编译器很可能为了优化程序的执行效率进行指令重排序,即对 2 和 3 进行互换
memory = allocate(); // 1:分配对象的内存空间instance = memory; // 3:设置 instance 指向内存空间 // 注意,此时对象还没有被初始化!ctorInstance(memory); // 2:初始化对象在进行了上述重排序的情况下,可能造成下面的执行顺序:
上述过程中,线程B 访问 instance 指向的对象的时候该对象尚未完成初始化,会引起各种幺蛾子的,请慎重。
解决办法:很简单,使用 volatile 修饰 instance 变量即可;
private volatile static Singleton instance = null;volatile 关键字通过提供内存避障的方式禁止指令重排序。
单线程中会不会得到一个尚未完全初始化的对象呢?(不会) 因为 java 语言规范中 intra-thread semantics(线程内语义)保证了重排序不会影响单线程程序的执行结果。怎么理解呢?单线程中,也允许上述重排序,但是当你使用 instance 引用的时候其指向的对象已经初始化成功了。
新闻热点
疑难解答