介绍
双重校验锁是单例模式中,饿汉式的一种实现方式。因为有两次判空校验,所以叫双重校验锁,一次是在同步代码块外,一次是在同步代码块内。
为什么在同步代码块内还要再检验一次?
第一个if减少性能开销,第二个if避免生成多个对象实例。
现有三个线程A,B,C,假设线程A和线程B同时调用getSingleton()时,判断第一层if判断都为空,这时线程A先拿到锁,线程B在代码块外层等待。线程A进行第二层if判断,条件成立后new了一个新对象,创建完成,释放锁,线程B拿到锁,进行第二层if判断,singleton不为空,直接返回singleton释放锁,避免生成多个对象实例。线程线C调用getSingleton时第一层判断不成立,直接拿到singleton对象返回,避免进入锁,减少性能开销。
为什么要用volatile关键字?
singleton = new Singleton();这行代码并不是一个原子指令,可能会在JVM中进行指令重排;
new 实例背后的指令,我们通过使用 javap -c指令,查看字节码如下:
// 创建 Singleton 对象实例,分配内存 0: new #5 // 复制栈顶地址,并再将其压入栈顶 3: dup // 调用构造器方法,初始化 Singleton对象 4: invokespecial #6 // Method "<init>":()V // 存入局部方法变量表 7: astore_1
从字节码可以看到创建一个对象实例,可以分为三步:
(1)分配对象内存(给singleton分配内存)。
(2)调用构造器方法,执行初始化(调用 Singleton 的构造函数来初始化成员变量)。
(3)将对象引用赋值给变量(执行完这步 singleton 就为非 null 了)。
在 JVM 的即时编译器中存在指令重排序的优化。指令重排并不影响单线程内的执行结果,但是在多线程内可能会影响结果。也就是说上面的2和3的顺序是不能保证的,但是并不会重排序 1 的顺序,因为 2,3 指令需要依托 1 指令执行结果。最终的执行顺序可能是 1-2-3 也可能是 1-3-2。
1-3-2的情况
上面多线程执行的流程中,如果线程A获取到锁进入创建对象实例,这个时候发生了指令重排序。当线程A 执行到 t3 时刻(singleton已经非null了,但是却没有初始化),此时线程 B 抢占了,由于此时singleton已经不为 Null,会直接返回 singleton对象,然后使用singleton对象,然而该对象还未初始化,就会报错。我们只需将 singleton 变量声明成 volatile 就可以禁止指令重排,避免这种现象发生。
参考/好文:
菜鸟教程 – 设计模式--https://www.runoob.com/design-pattern/singleton-pattern.html
掘金 --https://juejin.im/post/5d54c2d251882542f27bdff6