个人博客

http://www.milovetingting.cn

单例模式

模式介绍

整个系统只需要拥有一个全局对象,这样有利于我们协调系统整体的行为。

定义

确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

实现单例模式主要有如下几个关键点:

  • 构造函数不对外开放,一般为private

  • 通过一个静态方法或者枚举返回单例类对象

  • 确保单例类的对象有且只有一个,尤其是在多线程环境下

  • 确保单例类对象在反序列化时不会重新构建对象

单例模式的写法

  • 饿汉式

在声明静态对象时已经初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Singleton {

private static final Singleton mInstance = new Singleton();

private Singleton() {

}

public static Singleton getInstance() {
return mInstance;
}

}
  • 懒汉式

懒汉模式是声明一个静态对象,并且在用户第一次调用getInstance时进行初始化。优点是:单例只在使用时才会被实例化。缺点是:第一次加载需要及时进行实例化,反应稍慢,每次调用时都进行同步,造成不必要的同步开销。这种模式不建议使用。

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

private static volatile Singleton mInstance;

private Singleton() {

}

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

}
  • DCL(Double Check Lock)实现单例

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Singleton {

private static volatile Singleton mInstance;

private Singleton() {

}

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

}
  • 静态内部类单例模式

当第一次加载Singleton类时并不会初始化sInstance,只有在第一次调用Sington的getInstance方法时才会导致sInstance被初始化。这是推荐使用的单例模式实现方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton {

private Singleton() {

}

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

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

}
  • 枚举单例

在上述的几种单例模式实现中,在反序列化的情况下,它们就会出现重新创建对象。

序列化可以将一个单例的实例对象写到磁盘,然后再读回来,从而有效地获得一个实例。即使构造函数是私有的,反序列化时依然可以通过特殊的途径去创建一个新的实例。反序列化操作提供了一个特别的钩子函数,类中具有一个私有的readResolve()函数,这个函数可以让开发人员控制对象的反序列化。如果要杜绝对象在反序列化时重新生成对象,必须加入readResolve函数。而枚举则不存在这个问题。

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

对于序列化,有两点需要注意:

  1. 可序列化类中的字段类型不是Java的内置类型,那么该字段也需要实现Serializable接口

  2. 如果调整了可序列化类的内部结构,如新增,去除某个字段,但没有修改serialVersionUID,那么会引发java.io.InvalidClassException异常或者导致某个属性为0或者null。此时的最好方案是直接将serialVersionUID设置为0L,这样即使修改类的内部结构,反序列化也不会报错,只是新修改的字段会为0或者null。

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

1
2
3
4
5
6
7
8
9
10
public enum Singleton {

INSTANCE;

public void doSomething()
{

}

}
  • 使用容器实现单例模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class SingletonManager<T> {

private static Map<String, Object> objMap = new HashMap<>();

private SingletonManager() {

}

public static void registerService(String key, Object instance) {
if (!objMap.containsKey(key)) {
objMap.put(key, instance);
}
}

public static Object getService(String key) {
return objMap.get(key);
}

}

在程序的初始阶段,将多种单例类型注入到一个统一的管理类中,在使用时根据key获取对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行获取操作,降低了用户的使用成本,也对用户隐藏具体实现,降低耦合度。