DCL(Double Check Lock) + volatile 实现单例
常见单例实现的方式分为饿汉式与懒汉式。
- 饿汉式
从字面意思不难理解,如同饿汉一样,一上来就抱着馒头啃。 - 懒汉式
而懒汉式就比较优雅了,饿了才去找馒头吃。
- 饿汉式实现的方式:
- 静态常量
- 静态代码
- 块枚举
- 懒汉式实现方式:
- 静态工厂方法(非线程安全)
- 静态工厂方法 + synchronized(线程安全,效率不高)
- 静态工厂方法 + synchronized + if判断(效率解决了,但是线程不安全)
- DCL双重效应(看似效率不错,线程也安全。但是它会导致对象未初始化)
- DCL + volatile(线程安全,效率不错。说synchronized效率低的,请查询下JDK1.5后对synchronized锁优化、锁升级过程)
本文将主要讲解DCL + volatile 实现单例原理,其他方式本文将不再做过多陈述。希望能用简单的白话,让大家能清晰易懂。
在编写代码之前,我们先简单了解下 volatile 关键字特性。
1.保证多线程下变量的可见性
2.禁止指令重排
3.不保证原子性
这里就不详细讲解volatile原理了,这不是本文重点,我们只需指定它的作用便可,若感兴趣的下方留言,后期我可专门发布一篇刨析volatile原理文章。
好啦,废话说了一大堆,直接上干货。
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {
}
public static Singleton getInstance() {
if (null == singleton) { // 1-1,2-1
synchronized (Singleton.class) { // 1-2,2-2(wait)
if (singleton == null) { // 1-3,2-3
singleton = new Singleton(); // 1-4
}
}
}
return singleton;
}
}
这里我们假设1-1 表示 线程1 步骤1,2-1表示 线程2 步骤1。
步骤 1:当线程1和2同时进入,首先进行if判断,发现是null那么继续向下执行。(这里的判断不难理解,如果有就返回,没有就创建)
步骤 2:此时线程1和2进行抢占锁,这里假设线程1抢到锁,线程2需等待。
步骤 3:线程1先进入步骤3,判断是否为null,为null便进行创建对象;然后释放锁,返回对象。(为什么需要再次判断?如果不进行判断,线程1执行完后,线程2进来也会再次创建对象)
这么分析好像发现DCL看似没有任何毛病,但是可别忘记为什么加volatile。我们来简单的再次分析下,new 一个对象的过程。
- 在堆区分配对象所需要的内存(JVM内存分:堆区、方法区、栈。他们各自区别,可参考此篇文章)
- 变量赋默认值
- 执行构造方法,初始化
- 返回引用地址
我们知道CPU执行是无序的,往往代码若重排后将会打乱执行的顺序,使程序出现不符合预期的结果。如果创建对象指令被重新排列可能会出现如:1–>2–>4–>3的执行情况。那么我们上面代码的执行可能会出现如下结果:
线程1,在执行第4步时,发生指令重排(1–>2–>4–>3),此时线程1返回的引用地址,而线程2执行步骤3进行判断发现不为空,直接进行返回了,而返回的对象并没有进行初始化。接着报错对象尚未初始化。
至此各位知道DCL模式为什么必须要加volatile关键字了吧。