首先是代码,经典的双重锁写法。
public class Singleton {
private volatile static Singleton Instance;
private Singleton(){}
public static Singleton getInstance(){
if(Instance == null){
synchronized(Singleton.class){
if(Instance == null){
Instance = new Singleton();
}
}
}
return Instance;
}
}
先说为什么需要两次判空的原因,第一次判断是为了验证是否创建对象,避免多线程访问时每个线程都加锁,提升效率第二次判断是为了避免重复创建单例,因为可能会存在多个线程通过了第一次判断在等待锁,来创建新的实例对象。
例如:有三个线程,A与B同时调用getSingleton时,判断第一个if都为空,这时A拿到锁,进行第二层if判断,条件成立new了一个对象;由于Synchronized原因,B在外层等待,A创建完成,释放锁,B拿到锁,进行第二层if判断,条件不成立,结束释放锁。C调用getSingleton时第一层判断不成立,直接拿到singleton对象返回,避免进入锁,减少性能开销。
在说说为什么有了Synchronized却还需要volatile去修饰Instance。volatile修饰变量只是为了禁止指令重排序,因为在 Instance = new Singleton(); 创建对象时,底层会分为四个指令执行:(下面是正确的指令执行顺序)
①、如果类没有被加载过,则进行类的加载
②、在堆中开辟内存空间 adr,用于存放创建的对象
③、执行构造方法实例化对象
④、将堆中开辟的内存地址 adr 赋值给被volatile修饰的Instance引用变量
如果Instance引用变量不使用volatile修饰的话,则可能由于编译器和处理器对指令进行了重排序,导致第④步在第③步之前执行,此时Instance引用变量不为null了,但是Instance这个引用变量所指向的堆中内存地址中的对象是还没被实例化的,实例对象还是null的;那么在第一次判空时就不为null了,然后去使用时就会报NPE空指针异常了。