DCL单例模式,如何解决DCL问题

何为DCL,DCL即Double Check Lock,双重检查锁定。下面从几个单例模式来讲解

懒汉式

public void Singleton{
    private static Singleton singleton;

    private Singleton(){}

    public static Singleton getInstance(){
        if(singleton==null){
               singleton=new Singleton();
        }

            return singleton;
    }

        

}

这种方法在单线程下是可取的,但是在并发也就是在多线程的情况下是不可取的,因为其无法保证线程安全,优化如下:

public void Singleton{
    private static Singleton singleton;

    private Singleton(){}

    public synchronized static Singleton getInstance(){
        if(singleton==null){
               singleton=new Singleton();
        }

        return singleton;
    }

}

优化非常简单,在getInstance方法上加上了synchronized同步,尽管jdk6以后对synchronized做了优化,但还是会效率较低的,性能下降。那该如何解决这个问题?于是有人就想到了双重检查DCL

public void Singleton{
    private static Singleton singleton;

    private Singleton(){}

    public static Singleton getInstance(){
        if(singleton==null){
            synchronized(Singleton.class){
                if(singleton==null)  singleton=new Singleton();
            }
              
        }
        return singleton;
    }

}

这个代码看起来perfect:

  1. 如果检查第一一个singleton不为null,则不需要执行加锁动作,极大的提高了性能
  2. 如果第一个singleton为null,即使有多个线程同时判断,但是由于synchronized的存在,只有一个线程能创建对象
  3. 当第一个获取锁的线程创建完成singleton对象后,其他的在第二次判断singleton一定不会为null,则直接返回已经创建好的singleton对象

DCL看起来非常完美,但其实这个是不正确的。逻辑没问题,分析也没问题?但为何是不正确的?不妨我们先回顾一下创建对象的过程

  1. 为对象分配内存空间
  2. 初始化对象
  3. 将内存空间的地址赋值给对应的引用

但由于jvm编译器的优化产生的重排序缘故,步骤2、3可能会发生重排序:

  1. 为对象分配内存空间
  2. 将内存空间的地址赋值给对应的引用
  3. 初始化对象

如果2、3发生了重排序就会导致第二个判断会出错,singleton != null,但是它其实仅仅只是一个地址而已,此时对象还没有被初始化,所以return的singleton对象是一个没有被初始化的对象

知道问题的原因,那么我们就可以解决?

不允许重排序

重排序不让其他线程看到

解决方法

利用volatile的特性即可阻止重排序和可见性

public class Singleton {
   //通过volatile关键字来确保安全
   private volatile static Singleton singleton;

   private Singleton(){}

   public static Singleton getInstance(){
       if(singleton == null){
           synchronized (Singleton.class){
               if(singleton == null){
                   singleton = new Singleton();
               }
           }
       }
       return singleton;
   }
}

类初始化的解决方案

public class Singleton {
   private static class SingletonHolder{
       public static Singleton singleton = new Singleton();
   }

   public static Singleton getInstance(){
       return SingletonHolder.singleton;
   }
}

该解决方案的根本就在于:利用classloder的机制来保证初始化instance时只有一个线程。JVM在类初始化阶段会获取一个锁,这个锁可以同步多个线程对同一个类的初始化。

Java语言规定,对于每一个类或者接口C,都有一个唯一的初始化锁LC与之相对应。从C到LC的映射,由JVM的具体实现去自由实现。JVM在类初始化阶段期间会获取这个初始化锁,并且每一个线程至少获取一次锁来确保这个类已经被初始化过了。

 

你可能感兴趣的:(JAVA,concurrent)