何为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:
- 如果检查第一一个singleton不为null,则不需要执行加锁动作,极大的提高了性能
- 如果第一个singleton为null,即使有多个线程同时判断,但是由于synchronized的存在,只有一个线程能创建对象
- 当第一个获取锁的线程创建完成singleton对象后,其他的在第二次判断singleton一定不会为null,则直接返回已经创建好的singleton对象
DCL看起来非常完美,但其实这个是不正确的。逻辑没问题,分析也没问题?但为何是不正确的?不妨我们先回顾一下创建对象的过程
- 为对象分配内存空间
- 初始化对象
- 将内存空间的地址赋值给对应的引用
但由于jvm编译器的优化产生的重排序缘故,步骤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在类初始化阶段期间会获取这个初始化锁,并且每一个线程至少获取一次锁来确保这个类已经被初始化过了。