第003弹:懒汉型单例模式的演变

这篇文章主要是为了从头开始,详细介绍懒汉模式的实现,以及实现的缘由。html

以前写过一篇比较浅的懒汉模式,能够优先参照:设计模式(一)单例模式:2-懒汉模式java

 

Step1:基础的懒汉模式设计模式

public class LazySingleton {

    private static LazySingleton instance = null;

    private LazySingleton() {
    }

    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

基础的懒汉模式保证了在调用 getInstance() 方法的时候才第一次初始化单例对象。多线程

可是这么作没法保证在多线程环境下只建立一个对象。post

显然,假设有多个线程同时调用 getInstance() 方法,在第一个线程执行完毕以前,会有多个 LazyInstance 对象被建立。性能

 

Step2:为 getInstance() 方法加上同步锁url

public class LazySingleton {

    private static LazySingleton instance = null;

    private LazySingleton() {
    }

    public synchronized static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

经过简单地在方法上加上同步锁,能够保证同时只有一个线程调用这个静态方法,从而保证在多线程环境下的单例。spa

然而这么作有明显的 performance 隐患。线程

假设有多个线程想要获取 instance,不管此时对象是否已经被建立,都要频繁地获取锁,释放锁。这种作法很影响效率。翻译

 

Step3:在 getInstance() 方法内部增长同步代码块

public class LazySingleton {

    private static LazySingleton instance = null;

    private LazySingleton() {
    }

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

既然在方法上加同步锁不合适,那么就在方法内部增长同步代码块。

在判断 instance == null 以后,增长的同步代码块就不会产生 performance 问题,由于以后的访问会直接 return,不会进入同步代码块。

可是这么作,不能完整地保证单例。

参照 Step1,假设有多线程调用,且都经过了 instance == null 的判断,那么同样会有多个 LazySingleton 对象被建立。

 

Step4:使用 Double-Checked Locking

public class LazySingleton {

    private static LazySingleton instance = null;

    private LazySingleton() {
    }

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

经过增长双重判断,以及同步代码块,就能够避免 Step3 中可能出现的隐患。

可是 Double-Checked Locking 虽然可以保证单例的建立,可是在多线程的状况下可能出现某个线程使用建立不彻底的对象的状况。

 

Step5:使用 volatile 关键字修饰字段 instance

public class LazySingleton {

    private static volatile LazySingleton instance = null;

    private LazySingleton() {
    }

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

 

参考文档:The "Double-Checked Locking is Broken" Declaration

若是不适应英文描述,ImportNew 对这篇文档进行了翻译:能够不要再使用Double-Checked Locking了

 

这里面讲述了 Double-Checked Locking 在懒汉模式下可能出现的问题。

主要问题在于 Java 指令重排。

当 Java 代码被编译器翻译成字节码被存储在 JVM 时,为了提升性能,编译器会对这些操做指令进行指令重排。

也就是说,代码在计算机上执行的顺序,会被打乱。

返回到本例的问题,懒汉模式最关键的2个操做:

  1. 在 heap 中建立一个 LazyInstance 对象。
  2. 为字段 instance 赋值。

假设操做1在操做2以前被执行,那么代码就没有问题。

反之若操做2在操做1以前被执行,若是不能保证建立 LazyInstance 对象的过程是原子的,那么代码仍是会出现问题,由于 instance 指向了一个没有被建立彻底的对象。

事实上,引用类型和64位类型(long 和 double)都不能被原子地读写。

解决方案是经过 volatile 关键字来禁止指令重排(这是 volatile 的两个做用之一,另外一个做用是保证共享变量的可见性,这里不深刻展开)

相关文章
相关标签/搜索