单例模式的实现方式

单例模式介绍

单例模式是应用最广的模式之一,在应用这个模式时,单例对象的类必须保证只有一个实例存在。
如在一个应用中,应该只有一个 ImageLoader 实例,这个 ImageLoader 中又包含有线程池、缓存系统、网络请求等,很消耗资源,因此,没有理由让它构造多个实例。

单例模式的使用场景

确保某个类有且只有一个对象的场景,避免产生多个对象消耗过多的资源,或者某种类型的对象应该有且只有一个。

单例模式 UML 类图

实现方式

  • 简单的懒汉模式

懒汉模式在用户第一次调用 getInstance() 时进行初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton {
private static Singleton mInstance;
/**
* 构造函数私有化
*/
private Singleton() {

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

优点是单例只有在使用时才会被实例化,在一定程度上节约了资源;
缺点是每次调用 getInstance() 都进行同步,造成不必要的同步开销。这种模式一般不建议使用。

  • Double Check Lock(DCL)实现单例
    DCL 方式实现单例模式的优点是既能够在需要时才初始化单例,又能保证线程安全,且单例对象初始化后不进行同步锁。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Singleton {
private static Singleton mInstance;
/**
* 构造函数私有化
*/
private Singleton() {
}
public static Singleton getInstance() {
if (mInstance == null) {
synchronized (Singleton.class) {
if (mInstance == null) {
mInstance = new Singleton();
}
}
}
return mInstance;
}
}

可以看到进行了两次非空判断,第一层判断主要是为了避免不必要的同步,只有实例第一次被访问时,才会有线程进入同步块,这样极大提高了性能。避免了synchronized带来的较大性能损失。

第一次访问时,如果有多个线程同时进入if块,只有第一个线程会获得锁,其他线程被阻塞,第一个线程可以创建实例,退出 synchronized 。被阻塞的线程会进入同步块,进行第二次check,如果此时实例不为null,则返回。

仔细一想,这个代码挺完美的,但是不是这个样子的,具体问题出现在哪呢?

Java程序创建一个实例的过程为: mInstance = new Singleton() 语句,这里看起来是一句代码,但实际上它并不是一个原子操作,这句代码最终会被编译成多条汇编指令,它大致做了3件事情:
(1)给 Singleton 的实例分配内存
(2)调用 Singleton() 的构造函数,初始化成员字段;
(3)将 mInstance 对象指向分配的内存空间(此时 mInstance 就不是 null 了)。

但是,由于 Java 编译器允许处理器乱序执行,上面的第二和第三的顺序是无法保证的。也就是说,执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,并且在 3 执行完毕、2 未执行之前,被切换到线程 B 上,这时候 mInstance 因为已经在线程 A 内执行过了第三点,mInstance 已经是非空了,所以,线程 B 直接取走 mInstance,再次使用时就会出错,这就是 DCL 失效问题。

在 JDK 1.5 之后,SUN官方已经注意到这种问题,调整了 JVM,具体化了 volatile 关键字,
用该关键字修饰的变量在被变更时会被其他变量可见,最主要的是防止了重排序。因此在 JDK1.5 之后只需要改成 private volatile static Singleton mInstance; 就可以保证 mInstance 对象内存都是从主内存中读取,就可以使用 DCL 的写法来完成单例模式。
当然,volatile 或多或少也会影响性能,但考虑到程序的正确性,牺牲着点性能耗时值得的。

DCL 的优点:资源利用率高,第一次执行 getInstance 时单例对象才会被实例化,效率高。
缺点:第一次加载时反应稍慢,也由于 JMM 的原因偶尔会失败。在高并发环境下有一点的缺陷。

  • 静态内部类单例模式
1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {
private Singleton() {
}

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

private static class SingletonHolder {
private static final Singleton mInstance = new Singleton();
}
}

当第一次加载 Singleton 类时并不会初始化 mInstance,只有在第一次调用 Singleton 的 getInstance 方法时才会导致 mInstance 被初始化。因此,第一次调用 getInstance 方法会导致虚拟机加载 SingletonHolder 类,这种方式不仅能够确保线程安全,也能够保证单例对象的唯一性,同时也延迟了单例的实例化,所以这是推荐使用的单例实现方式。

  • 枚举单例
1
2
3
public enum SingletonEnum {
INSTANCE;
}

写法简单是枚举单例最大的优点,枚举在 java 中与普通的类是一样的,不仅能够有字段,还能够有自己的方法。最重要的是默认枚举实例的创建是线程安全的,并且在任何情况下它都是一个单例。

------ 本文结束,欢迎阅读------
欢迎关注公众号,不定期更新