创建型模式之单例模式

一、什么是单例模式?

wiki 给出的定义如下:单例模式,也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。

  • 比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

二、为什么要使用单例模式?

  • 有一些对象的内存消耗比较大,创建多次会造成很大的资源开支。
  • 方便配置。
    • 例如 网络请求经常要用到 cookie,如果使用 OkHttp 可以将其封装为一个单例工具类,内部使用 CookJar 对 cookie 进行管理,这样后续的请求就会方便很多。
  • 安全性。比如 SQLite 数据库的增删查改,通过单例对象对外提供增删查改功能,可以避免一些并发错误。

三、单例模式的实现方式

1. 饿汉模式

1
2
3
4
5
6
7
8
9
10
11
public class Singleton {
private static Singleton sInstance = new Singleton();
private Singleton(){
}
public static Singleton getInstance() {
return sInstance;
}
}

在声明静态对象时就把它初始化。(因为饿,所以迫不及待先把它 new 出来)

2. 懒汉模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton {
private static Singleton sInstance;
private Singleton() {
}
public static synchronized Singleton getInstance() {
if (sInstance == null) {
sInstance = new Singleton();
}
return sInstance;
}
}

getInstance 方法中添加了 synchronized 关键字,也就是

  • 一个问题:即使 instance 已经被初始化,之后的每次调用还是会 进行同步,这样会消耗不必要的资源

为什么叫做懒汉呢?其实可以理解为懒加载,有需要的时候,再把它加载出来。

懒汉单例模式的

  • 优点:单例只有在使用时才会被实例化,在一定程度上节约了资源。
  • 缺点:第一次加载时需要及时地实例化,反应稍慢,最大的问题是:每次调用都会进行同步,造成不必要的同步开销

3. 双重校验锁 ( DCL )

DCL 既能够在需要时才初始化实例,又能够保证线程安全,而且单例对象初始化后才调用 getInstance 不进行同步锁。

1
2
3
4
5
6
7
8
9
10
public static Singleton getInstance() {
if (sInstance == null) {
synchronized (Singleton.class) {
if (sInstance == null) {
sInstance = new Singleton();
}
}
}
return sInstance;
}

为什么在同步块内还要再进行判空?

因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了

为什么要加 volitale 关键字?

sInstance = new Singleton();这句代码会被编译成多条汇编指令,它大致做了 3 件事情

  1. 给 Singleton 实例分配内存
  2. 调用 Singleton 的构造函数,初始化成员字段
  3. 将 sInstance 对象指向分配的内存空间

但是由于 Java 编译器允许处理器乱序执行,以及 JDK 1.5 之前 JMM(Java Memory Model, Java 内存模型) 中 cache、寄存器到主内存回写顺序的规定,
上面的 2 和 3 的顺序是无法保证的。 如果是 1-3-2,那么当 3 执行完,并且 2 还没有执行的话,被切换到到线程 B,这时 sInstance 已经非空(因为 3 已经被执行了嘛),若 此时线程 B 直接取走 sInstance 使用时就会报错。

解决:JDK 1.5 之后,SUN 官方调整了 JVM,具体化了 volatile 关键字(禁止指令重排序)。

  • 如果是在 JDK 1.5 之后,那么只需要把 sInstance 的声明 改为 private volatile static Singleton sInstance; 即可

4. 静态内部类单例模式

DCL 在某些情况下会出现失效的问题。这个问题被称为双重检查锁定(DCL)失效。

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton{
private Singleton(){ }
public static Singleton getInstance(){
return SingletonHolder.sInstance;
}
private static class SingletonHolder {
private static final Singleton sInstance = new Singleton();
}
}

第一次调用 Singleton 的 getInstance 方法时才会导致 sInstance 被初始化。因此,第一次调用 getInstance 方法会导致虚拟机加载 SingletonHolder 类,这种方式不仅能够确保线程安全,也能保证单例对象的唯一性。

所以这是推荐使用的单例模式实现方式

5. 枚举单例

对枚举不了解可以先看看枚举这篇文章

写法简单是枚举单例 最大的优点。

1
2
3
public enum Singleton{
INSTANCE;
}

获取对象可以这么写: Singleton singleton = Singleton.INSTANCE;

枚举在 Java 中与普通的类是一样的,不仅能够有字段,还能定义自己的方法。

1
2
3
4
5
6
7
8
9
10
public enum Singleton{
INSTANCE;
//枚举内部可以定义成员;
private String mString;
//枚举内部可以定义方法;
public void doSth() {
//do sth
}
}

最重要的是默认枚举实例的创建是线程安全的,并且任何情况下它都是一个单例。

上述几种方式中,在一个情况下都会重新创建对象的情况,那就是反序列化

即使构造函数是私有的,反序列化是依然可以通过特殊的途径去创建类的一个新的实例,相当于 调用该类的构造函数。

反序列化操作提供了一个很特别的钩子函数,类中具有一个私有的、被实例化的方法 readResolve(),这个方法可以让开发人员控制对象的反序列化。

上述几种单例方法中如果要杜绝单例对象在被反序列化时重新生成对象,那必须加入如下方法:

1
2
3
private Object readResolve() throws ObjectStreamException {
return sInstance;
}

也就是在 readResolve 方法中将 sInstance 对象返回,而不是默认的重新生成一个新的对象。

对于枚举,并不存在这个问题,因为即使反序列化也不会重新生成新的实例

6. 使用容器实现单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SingletonManager {
private static Map<String, Object> sObjectMap = new HashMap<>();
private SingletonManager() {
}
public static void registerService(String key, Object instance) {
if (!sObjectMap.containsKey(key)) {
sObjectMap.put(key, instance);
}
}
public static Object getService(String key) {
return sObjectMap.get(key);
}
}

在程序的初始,将多种单例类型注入到一个统一的管理类中,使用时格根据 key 获取对象对应类型的对象。

  • 这种实现方式主要是方便对单例对象进行统一管理。

小结

  • 不管使用哪一种形式实现单例模式,核心原理都是==将构造函数私有化==,并且==通过静态方法获取唯一的的实例==。
  • 在获取的过程中必须保证线程安全防止反序列化导致重新生成实例对象等问题。

使用时的注意点

避免内存泄漏

  • Android 开发中使用单例模式,如果获取单例对象需要使用 Context,那么尽量使用 ApplicationContext(只要用 context.getApplicationContext() 即可获取)。
    因为如果使用其他 Context(如 Activity) 可能会造成 Activiity 生命周期 执行完成之后,因为其引用被单例所持有,而无法被回收。
  • 多进程环境下,单例模式会失效。

Android 源码中的单例模式简述

我们经常会通过 Context 去获取系统服务,如 LayoutInflater、NetworkStatsManager,这些服务在创建时会以键值对的形式缓存到 HashMap 中,便于管理。

需要时就通过调用 context.getSystemService(String name) 方法获取 。首先会以 name 作为 key,到 hashMap 中查找中相应的服务,如果对应的服务为 null 就创建一个实例,并将该实例缓存到 HashMap 中;如果对应的服务已经存在,则直接返回。

参考资料与学习资源推荐

Show Comments
0%