DCL:double check lock双重检查锁,如下面的代码
public class Singleton {
private static volatile Singleton INSTANCE = null;
private Singleton(){}
public static Singleton getInstance(){
if(INSTANCE==null){//第一次检查
synchronized (Singleton.class){
if(INSTANCE==null){//第二次检查
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
为什么要加两次判空,第一次判空能不能不加?
效率问题:假设第一次判空不加,那么每次进入这个方法,INSTANCE不论是不是null,都会执行下面的synchronized代码块,多线程下会出现锁的竞争,而除了第一次初始化,后面的都不会为null,判空的效率比加锁高。
为什么要进行第二次判空?
防止多次初始化:多线程下,有可能会出现两个线程都经过了前面第一次检查,来到了下面的synchronized这里,如果不判空,就会出现一个线程new了一个Singleton出来,然后释放锁,第二个线程进来又会new一个Singleton出来。
volatile作用:1. 保持内存可见性,2.防止指令重排序
volatile这里的作用就是防止指令重排
INSTANCE = new Singleton();
当使用new
关键字创建一个对象时,JVM需要做哪些事情
1.为对象分配内存
1.优先栈上分配
2.栈中不能分配的,对象是否足够大?大的直接分配进老年代。
3.TLAB中是否可以快分配?
4.不能则在Eden区慢分配。
2.内存分配完毕,属性设置默认值(引用类型为null,基本类型为对应的默认值)。
3.执行构造函数,属性设置初始值。
4.建立连接,引用指向对象内存地址。
可以看到,new一个对象,虽然只有一行代码,实际上需要经过好几个过程,而且这些过程并非顺序执行。
有可能一个对象在未初始化时,就先建立连接了,一旦发生这种情况,使用DCL实现的单例模式,就会导致线程拿到的是一个未被初始化的对象。
想想看,未被初始化的对象,属性为引用类型则值全部为null,一旦对这些属性进行了操作,则会抛出空指针异常
volatile修饰的语句则禁止CPU这种乱序执行,保证指令执行的顺序性。
new 对象时的汇编指令
public class TestNewInstance {
public static void main(String[] args) {
Object o = new Object();
}
}
将上面的代码编译后找到class文件所在目录,用javap -c TestNewInstance.class命令得到编译后的汇编指令。
0: new #2 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: astore_1
8: return
顺序的情况下,new指令申请了一块内存空间,invokespecial调用构造方法为对象进行初始化,astore_1将变量和新创建的对象关联起来。但是invokespecial和astore_1这两条指令没有关联性,所以astore_1有可能会跑到invokespecial前面执行。
如图,假设两个线程,线程1执行完astore_1时(此时instance已经指向一块内存地址,不为null,但是对象还未完成初始化),CPU切换到线程2执行if(instance==null),结果为false,于是返回了一个不完整的对象。使用volatile禁止指令重排就可以避免这种情况发生。
为什么volatile能禁止指令重排?
volatile的底层,与现在的CPU有关系,用cpu原语实现的,loadFence原语,storeFence原语