对于单例的双重检锁,为什么要对变量加上 volatile 修饰关键字?缓存
要理解一个对象的建立过程,须要从运行时数据区进行分析,首先须要对JVM运行时数据区布局有深刻的理解,同时掌握类加载过程当中各个阶段的行为。详见后续的《从JVM角度分析 new 一个对象的详细过程》。bash
本文无需深刻分析这两个主题,只须要了解建立对象的整体流程便可。多线程
核心:非原子操做、指令重排序函数
private static volatile Singleton instance;
public static Singleton getInstance() {
if(instance == null) {
synchronized(Singleton.class) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
复制代码
避免重排序问题致使其余线程看到一个已经分配内存和地址,可是没有初始化的对象(对象还处于不可用状态),就被其余线程引用了(报异常)。布局
下面代码在多线程环境下不是原子操做ui
instance = new Singleton();
复制代码
这条指令坑被重排序,分以下两种可能的指令排序场景。spa
正常的底层执行顺序会分3步走:线程
一、给 instance 分配内存
二、调用实例 instance 的构造函数来初始化成员变量
三、将 instance 这个在栈中的引用,指向在步骤1和2中打包好的对象
复制代码
不管线程A当前执行到一、二、3哪一步,对于线程B,可能看到的 instance 的状态只有两种:null 和 非 null。code
步骤 1 和 2 中的 instance 对象都是 null 的,第3步看到的是非 null,对于正常顺序来讲,这是没问题的。对象
若是线程A 在重排序的状况下,可能会变成 1,3,2,假如线程A执行到第二步“3”时,instance 虽然已经不是 null,但还没初始化,不可用。
此时CPU时间片切换,从线程A 切换到线程B,线程B来调用 double check 这个 getInstance 单例方法,那么第一个 null check 时,看到的 instance 引用因为已经被线程 A 指向了内存块,不为 null,则直接返回这个instance。
可是,当使用这个对象的某个字段时,因为还没被初始化,处于不可用状态,会致使异常发生。
使用 volatile 修饰成员变量,那么在变量赋值时,会有一个内存屏障,也就是说只有执行完123步操做以后,其余线程读取操做时才能看到 instance 这个变量的值,不会形成误判,解决了对象状态不完整的问题。
同时,volatile 会强制将缓存中修改的数据刷新到主内存中,确保对其余线程的可见性。
此时,invalidate 其余CPU的缓存行,当其余CPU须要使用这个缓存行的变量时,就会去从新到主内存读取,保证数据是最新的。