谈谈单例

DCL

dcl 实现的单例模式的一种可靠方式是添加 volatile关键字 。

volatile 有如下的特点:

  1. 有序性,防止重排序
  2. 可见性 ,屏蔽local cache,变量的操作对于主存立即可见

在java中,对象的创建有如下的几个步骤

  1. 分配对象内存空间
  2. 初始化对象
  3. 设置instance指向刚刚分配的内存地址, 此时instance != null (重点)

只有这三个步骤完成了,才算是一个对象的初始化完成。

计算机指令 CPU 和编译器为了提高执行效率,会对代码进行优化 即重排序 ,只要最终的输出结果是正确的即可。

那么在对象创建的过程中,有可能编程了如下的顺序

  1. 分配对象内存空间
  2. 设置instance指向刚刚分配的内存地址, 此时instance != null (重点)
  3. 初始化对象

当第一个线程拿到锁并且进入到第二个if方法后, 先分配对象内存空间, 然后再instance指向刚刚分配的内存地址, instance 已经不等于null, 但此时instance还没有初始化完成。如果这个时候又有一个线程来调用getInstance方法, 在第一个if的判断结果就为false, 于是直接返回还没有初始化完成的instance, 那么就很有可能产生异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton {
private volatile static Singleton instance;

private Singleton(){}

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

在new关键字背后,有如下步骤与

  1. 在堆内存中创建对象实例

  2. 为实例对象赋初值

  3. 返回对象引用

静态内部类模式

1
2
3
4
5
6
7
8
9
10
11
12
public class CustomManager{
private CustomManager(){}

private static class CustomManagerHolder {
private static final CustomManager INSTANCE = new CustomManager();
}

public static CustomManager getInstance() {
return CustomManagerHolder.INSTANCE;
}
}

静态内部类的原理是:

当 SingleTon 第一次被加载时,并不需要去加载 SingleTonHoler,只有当 getInstance() 方法第一次被调用时,才会去初始化 INSTANCE,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。getInstance 方法并没有多次去 new 对象,取的都是同一个 INSTANCE 对象。

虚拟机会保证一个类的 () 方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 () 方法,其他线程都需要阻塞等待,直到活动线程执行 () 方法完毕

缺点在于无法传递参数,如Context等