双检锁/双重校验锁(DCL,即 double-checked locking)详细解析

1、介绍

单例模式是最常用的设计模式,并且双锁单例模式也是面试的常考题目。本文详细介绍了双锁单例模式,并总结了双锁单例模式的三个常考问题。

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();//erro
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {
        Singleton.getInstance();
    }
}

2、提出问题

  1. 为什么双重检验 (即为什么使用了两个if语句)
  2. 为什么加双锁(即为什么同时使用了synchronized关键字和volatile关键字)
  3. JDK版本为什么要大于1.5

2.1 为什么双重检验

    public static Singleton getInstance() {
        if (instance == null) {//线程1,2同时到达,均通过(instance == null)判断。
                                // 线程1进入下面的同步块,线程2被阻塞
            synchronized (Singleton.class) {
                if (instance == null) {//线程1执行发现instance为null,初始化实例后,释放锁。
                    // 线程2进入同步块,此次instance已经被初始化。无法通过if条件,避免多次重复初始化。
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

  执行双重检测是因为,如果多个线程通过了第一次检测,此时因为synchronized,其中一个线程会首先通过了第二次检测并实例化了对象,剩余的线程不会再重复实例化对象。这样,除了初始化的时候会加锁,后续的调用都是直接返回,解决了多余的性能消耗。

小结:

  • 外层判断:完成实例化后,之后的线程就不需要再执行synchronized等待,提高效率。
  • 内层判断:防止多次实例化。

2.2 为什么加双锁

                if (instance == null) {
                    instance = new Singleton();//erro
                }

如果不使用volatile关键字,隐患来自于上述代码中注释了 erro 的一行,这行代码大致有以下三个步骤:

  1. 在堆中开辟对象所需空间,分配地址
  2. 根据类加载的初始化顺序进行初始化
  3. 将内存地址返回给栈中的引用变量

由于 Java 内存模型允许“无序写入”,有些编译器因为性能原因,可能会把上述步骤中的 2 和 3 进行重排序,顺序就成了

  1. 在堆中开辟对象所需空间,分配地址
  2. 将内存地址返回给栈中的引用变量(此时变量已不在为null,但是变量却并没有初始化完成)
  3. 根据类加载的初始化顺序进行初始化

在这里插入图片描述
在这里插入图片描述
  通过对比发现,关键变化在于有volatile修饰的变量,赋值后(前面mov%eax,0x150(%esi)这句便是赋值操作)多执行了一个“lock addl$0x0,(%esp)”操作,这个操作的作用相当于一个内存屏障(Memory Barrier或Memory Fence,指重排序时不能把后面的指令重排序到内存屏障之前的位置)。

小结:
  这里简单解释一下,在putstatic操作之前设置内存屏障,保证之前的3步骤无法重排在2步骤之前。

2.3JDK版本大于1.5

  • volatile关键字的屏蔽指令重排的语义在JDK1.5中才被完全修复。
  • JDK5 以及后续版本扩展了volatile语义,不再允许volatile写操作与其前面的读写操作重排序,也不允许volatile读操作与其后面的读写操作重排序。
  • 32
    点赞
  • 62
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值