应用最广的模式-单例模式

单例模式的优点

单例对象的类保证只有一个实例存在。确保某个类有且仅有一个实例存在,避免产生多个对象消耗过多资源。

关键点

  • 构造函数私有, 不对外开放
  • 通过静态方法, 枚举或容器返回单例类对象
  • 确保单例类对象有且仅有一个, 尤其在多线程环境下
  • 确保单例类对象在反序列化时不会重新创建

实现方式:

饿汉式

在类初始化时就创建单例对象. 下面有两种实现方式:

静态对象单例

使用静态对象持有一个单例实例, 在类初始化时就创建这个单例对象.

1
2
3
4
5
6
7
8
9
public class Singleton{
private static INSTANCE = new Singleton();

private Singleton(){}

public static Singleton instance(){
return INSTANCE;
}
}

枚举单例

利用枚举的单一性, 又能允许存在方法和属性的特性.

1
2
3
4
5
6
7
8
9
public enum SingletonEnum {
INSTANCE;

void doSomething() {}
}

public static void main() {
SingletonEnum.INSTANCE.doSomething();
}

枚举单例相比静态对象的单例, 优点在于可以防止反序列化的时候创建新的对象.


饿汉式优点:

  • 简单
  • 线程安全(枚举单例也是)

饿汉式缺点:

  • 如果实例的构造较耗时, 会造成类的加载过程较耗时
  • 如果这个单例对象不会用到, 它也会一直存在, 造成内存的浪费

反序列化破解单例

当类实现了 Serializable 接口时, 类的对象就能被序列化和反序列化.但是当反序列化时, 获得的对象是重新分配的内存, 单例也就不存在了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public static void main(String[] args) {
Singleton singleton = Singleton.instance();
writeToFile(singleton, "singleton");
Singleton singletonCopy = (Singleton) readFromFile("singleton");
System.out.println(singleton == singletonCopy); //false => 两个不同对象
}

// 序列化到文件
public static void writeToFile(Object obj, String name) {
try {
FileOutputStream fOut = new FileOutputStream(name);
ObjectOutputStream oOut = new ObjectOutputStream(fOut);
oOut.writeObject(obj);
oOut.close();
} catch (IOException e) {
e.printStackTrace();
}
}

// 从文件反序列化
public static Object readFromFile(String path) {
try {
FileInputStream fIn = new FileInputStream(path);
ObjectInputStream oIn = new ObjectInputStream(fIn);
return oIn.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}

上面的例子就通过反序列化创建了一个新的对象, 绕过了单例的限制. 但是也有防止单例对象被反序列化的方法, 就是通过实现 readResolve 方法, 让该方法返回已有的单例对象.

1
2
3
public Object readResolve(){
return INSTANCE;
}

readResolve

可以看到 readResolve 在从外部流创建对象时会被调用, 能有效防止通过反序列化手段创建单例之外的对象.

懒汉式

针对饿汉式需要在类初始化时就创建对象, 出现了懒汉式的加载方法, 在需要时才进行单例对象的初始化. 主要有以下几种:

  • 同步锁实现
  • 双重锁检测实现 DoubleCheckLock
  • 静态内部类单例实现

同步锁实现

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

private static SingletonLazy _instance;

public static synchronized SingletonLazy getInstance() {
if (_instance == null) {
_instance = new SingletonLazy();
}
return _instance;
}
}

只有在第一次调用 SingletonLazy.getInstance() 时才创建单例对象, 之后再调用都不会创建新的.
这种懒汉式模式能正常的运行, 也符合了单例的条件, 全局唯一, 多线程安全.
但是存在性能上的问题, 因为直接对 getInstance 方法加锁, 会导致即使单例对象已经创建了, 每次获取单例对象都会进行同步锁获取和释放, 造成资源浪费.

双重锁检测 DoubleCheckLock

为了解决上面同步锁造成的性能问题, 于是出现了 DoubleCheckLock(DSL)机制, 只在对象没有被创建的情况下,对创建过程加同步锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SingletonLazy {
private SingletonLazy() { }

// volatile: 在 JDK 5 之前防止指令重排序
private static volatile SingletonLazy _instance;

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

这里对单例对象进行了两次判空:

  • 第一层判断是为了避免不必要的同步, 只在对象没有创建时才走到下面的同步代码块
  • 第二层判断是为了只在 null 的情况下才创建对象. 在多线程情况下,如果两个以上线程都已经运行至同步锁处,也就是都已经判断变量为空,如锁内不再次判断,会导致实例重复创建

静态内部类单例

DCL 单例模式, 虽然能满足要求, 但是不是最优雅的实现, 静态内部类单例则能利用 JVM 的类加载机制做到单例实现.

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

private static class SingletonHolder {
private final static SingletonByInner singleton = new SingletonByInner();
}

public static SingletonByInner getInstance() {
return SingletonHolder.singleton;
}
}

为什么静态内部类能实现线程安全的单例模式呢?

因为 JVM 的类加载机制规定, 只有以下 5 种情况才会进行立即对类进行初始化(加载, 验证, 准备需要在此之前完成) :

  • 遇到 new getstatic putstatic invokestatic 4 条字节码指令时, 如果类没有进行初始化, 则需要先触发初始化
    • 4 个字节码指令对应的 Java 场景是:
      • 使用 new 创建对象
      • 读取或设置一个类的静态字段
      • 调用一个类的静态方法时
  • 通过反射获取类时, 如果类没有初始化, 则需要触发初始化
  • 当初始化一个类, 发现其父类还未初始化时, 需要先初始化其父类
  • 当虚拟机启动时, 用户需要指定一个包含了 main 方法的主类, 虚拟机会先初始化主类
  • 使用 JDK 1.7 之后的动态支持时, 如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果是 REF_getStatic REF_putStatic REF_invokeStatic 的方法句柄, 并且这个方法句柄对应的类没有进行初始化, 则需要先进行初始化

外部类 SingletonByInner 的字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
private io.github.stefanji.singleton.SingletonByInner();
descriptor: ()V
flags: ACC_PRIVATE
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #2 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 4: 0

public io.github.stefanji.singleton.SingletonByInner getInstance();
descriptor: ()Lio/github/stefanji/singleton/SingletonByInner;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: invokestatic #3 // Method io/github/stefanji/singleton/SingletonByInner$SingletonHolder.access$100:()Lio/github/stefanji/singleton/SingletonByInner;
3: areturn
LineNumberTable:
line 11: 0

可以看到第 18 行执行了 invokestatic 指令, 是当调用 getInstance 方法时才会走到这里. 也就说明只有当调用 getInstance 时, 才会触发内部类的初始化操作, 而且 JVM 只会初始化一个类一次, 所以就保证了内部类的静态实例在 JVM 中只有一个.

使用容器实现单例模式

使用容器实现单例模式, 其实不是单例模式的具体实现, 而是一种组织多个单例对象的方式. 每个单例对象具体的实现方式可以是上面几种.
容器对应 Java 中的 Map List 等结构, 有时需要在全局维护多个单例时, 使用容器能方便管理.
比如 Android 中经常使用 Context 获取系统的一些服务, 其实这些服务在 Context 中都是以单例的方式存在的:

Context.getSystemService 方法:

1
2
3
4
5
@Override
public Object getSystemService(String name) {
// 调用 SystemServiceRegistry
return SystemServiceRegistry.getSystemService(this, name);
}

在加载 SystemServiceRegistry 时会将常用服务创建, 并储存到单例容器 SYSTEM_SERVICE_FETCHERS 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
final class SystemServiceRegistry {
private static final String TAG = "SystemServiceRegistry";

private static final HashMap<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS =
new HashMap<String, ServiceFetcher<?>>();

// Not instantiable.
private SystemServiceRegistry() { }

static {
// 比如注册 LAYOUT_INFLATER_SERVICE
registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
new CachedServiceFetcher<LayoutInflater>() {
@Override
public LayoutInflater createService(ContextImpl ctx) {
return new PhoneLayoutInflater(ctx.getOuterContext());
}});
}

public static Object getSystemService(ContextImpl ctx, String name) {
// 从单例容器中获取单例对象
ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
return fetcher != null ? fetcher.getService(ctx) : null;
}
}