写一个DCL懒汉单例式

DCL:Double Check Lock 双重检查锁定

首先一开始我们写一个懒汉单例式如下

package com.interview.bintest.singleton;

/**
 * 下面是DCL单例懒汉式的demo
 */
public class DCLSingleton {

    /**
     * 因为是单例式,所以构造方法肯定是私有化
     * 无法通过new的方式来创建对象
     */
    private DCLSingleton(){

    }

    /**
     * 创建一个私有化静态成员变量,用于指向创建出来的单例
     */
    private static DCLSingleton dclSingleton;

    /**
     * 创建一个  公共静态  方法,提供给外部类调用
     * 方法则返回单例式(方法内创建)
     */
    public static DCLSingleton getInstance(){
        //因为是懒汉式创建,所以我们需要判断一下是否已经被创建了
        //没有则创建一个,有则直接返回该实例
        if(dclSingleton==null){//A
            dclSingleton = new DCLSingleton();//B
        }
        return dclSingleton;
    }



}

但是上面的代码在遇到多线程的时候就会产生问题,当x线程到达注释A处,判断完毕,条件成立,此时JVM把cpu的资源切换给y线程。

y线程同样到达A处,因为x线程并没有创建实例,所以y执行了注释B处的代码,即完成了单例的创建。之后线程x被重新唤醒。

因为x线程已经判断完了if中的条件,并且成立,于是x线程也执行了注释B处的代码,又创建了一个单例。这样就产生了线程不安全的问题。

于是我们通过synchronized同步代码块来解决这个问题,代码如下

package com.interview.bintest.singleton;

/**
 * 下面是DCL单例懒汉式的demo
 */
public class DCLSingleton {

    /**
     * 因为是单例式,所以构造方法肯定是私有化
     * 无法通过new的方式来创建对象
     */
    private DCLSingleton(){

    }

    /**
     * 创建一个私有化静态成员变量,用于指向创建出来的单例
     */
    private static DCLSingleton dclSingleton;

    /**
     * 创建一个  公共静态  方法,提供给外部类调用
     * 方法则返回单例式(方法内创建)
     */
    public static DCLSingleton getInstance(){
        //因为是懒汉式创建,所以我们需要判断一下是否已经被创建了
        //没有则创建一个,有则直接返回该实例
        synchronized (DCLSingleton.class){
            if(dclSingleton==null){//A
                dclSingleton = new DCLSingleton();//B
            }
        }
        return dclSingleton;
    }



}

此时解决了上面的问题。但是新问题来了,因为synchronized的存在,每个线程在执行注释A的判断之前都会争抢锁,并且每个线程都要锁住了才能判断是否有实例存在。这样就导致了阻塞,因为同一时间下只能有一个线程执行synchronized里的语句,其余的线程都阻塞住。

但是我们不能将注释A出的if条件判断提到外面将synchronized代码块包裹住。问题还是一样的,假设俩个线程都通过了判断,其中一个线程先获得锁进行了创建,后一个线程因为过了判断,所以获得前一个线程释放的锁,又进行一次创建。

为了解决以上的问题,我们就需要进行两次判断,即双重检查锁定。代码如下

package com.interview.bintest.singleton;

/**
 * 下面是DCL单例懒汉式的demo
 */
public class DCLSingleton {

    /**
     * 因为是单例式,所以构造方法肯定是私有化
     * 无法通过new的方式来创建对象
     */
    private DCLSingleton(){

    }

    /**
     * 创建一个私有化静态成员变量,用于指向创建出来的单例
     */
    private static DCLSingleton dclSingleton;

    /**
     * 创建一个  公共静态  方法,提供给外部类调用
     * 方法则返回单例式(方法内创建)
     */
    public static DCLSingleton getInstance(){
        //因为是懒汉式创建,所以我们需要判断一下是否已经被创建了
        //没有则创建一个,有则直接返回该实例
        /*
        情况一:实例还未创建
            假设x线程通过了第一个条件判断,此时x线程被挂起,y线程也通过了第一个判断
            并且执行了同步代码块,创建了一个实例,释放锁
            之后x线程被唤醒,执行同步代码块,再一次判断是否有实例存在,因为y线程创建了
            所以x线程不创建,释放锁。
        情况二:实例已经创建
            所有线程都无需执行同步代码块,在第一次条件判断不成立后直接返回实例即可
         */
        if (dclSingleton==null){
            synchronized (DCLSingleton.class){
                if(dclSingleton==null){//A
                    dclSingleton = new DCLSingleton();//B
                }
            }
        }
        return dclSingleton;
    }



}

但是现在仍然有一个问题,那就是在执行注释B处的代码时,该行代码是非原子性操作(即运行中途可能会被切换到另一个线程)。

这行代码在底层被编译成了8条汇编指令。大致做了以下三件事

(1)给创建的实例分配内存

(2)调用实例构造器,完成初始化

(3)将实例对象指向分配的内存空间

因为JVM底层会优化指令,对指令进行重排序,以上的执行顺序就有可能变成:(1)(3)(2)

当按照(1)(3)(2)的顺序执行到(3)时,此时这个线程被挂起

另一个线程开始执行判断,这时候判断实例对象就不为空了,因为指向了一个地址。所以直接返回该实例对象,就会报错:对象尚未初始化。

所以为了解决这个问题:需要禁止指令重排。(我也考虑过怎么让这一个部分变成原子操作,即不允许被打断,不过线程调度是系统的事,好像不容易改变)

因此我们需要用到volatile关键字,它有个最重要的作用就是禁止指令重排,因此最终的代码如下

package com.interview.bintest.singleton;

/**
 * 下面是DCL单例懒汉式的demo
 */
public class DCLSingleton {


    private DCLSingleton(){

    }

    private static volatile DCLSingleton dclSingleton;

    public static DCLSingleton getInstance(){
        if (dclSingleton==null){
            synchronized (DCLSingleton.class){
                if(dclSingleton==null){//A
                    dclSingleton = new DCLSingleton();//B
                }
            }
        }
        return dclSingleton;
    }



}
原文地址:https://www.cnblogs.com/skyvalley/p/14244447.html