双重校验锁 --使用volatile和两次判空校验

介绍

双重校验锁是单例模式中,饿汉式的一种实现方式。因为有两次判空校验,所以叫双重校验锁,一次是在同步代码块外,一次是在同步代码块内。

为什么在同步代码块内还要再检验一次?

第一个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

希望本文章对您有帮助,您的转发、点赞是我的创作动力,十分感谢。更多好文推荐,请关注我的微信公众号--JustJavaIt
原文地址:https://www.cnblogs.com/liaowenhui/p/12772744.html