单例模式

1. 概述

单例模式,顾名思义,它可以保证在系统运行中,整个系统只存在某个类的 一个对象

该对象在系统中是 唯一的

2. 经典实现

需要注意的是,虽然这个实现很经典,但是它是错误的,在实际中不应该使用;

但是,由于它较为简单,所以拿这种实现来说明单例的原理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton {
// 存储自身的引用
private static Singleton instance = null;

private Singleton() {
// 不允许外界通过构造器构建
}

private static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}

该实现中,最为核心的就是 getInstance() 方法;

通过 if 的判断,如果实例为空,那么进行构造;

当实例构造完毕后,instance 就不为空了,直接返回自身。

这样就保证了在普通情况下,对象只会有一个。

但是,这种实现在多线程时不成立!

当有两个线程同时尝试获取单例实例时,就会造成对象的重复构建:

这是由于两个线程可能同时进入 if 区域中,导致两个线程分别执行对象的构建,最后就会出现两个单例对象。

3. 懒汉式

由于经典实现的缺陷是因为多线程导致的;

那么我们只要把 getInstance() 设置为同步方法不就完了吗?

1
2
3
4
5
6
7
private static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}

return instance;
}

但是,这种实现方法具有很严重的 性能缺陷

由于每次获取单例实例都需要先获取锁,导致性能低下;

而实际上,只需要在单例的构建过程进行同步即可;

没有必要在每次获取对象的时候都进行同步。

4. 饿汉式

既然多线程造成的问题是在 getInstance() 时发生的;

而使用 synchronized 关键字又存在性能缺陷;

那么,为什么不在类加载的一开始就进行实例构建呢?

这样,既避免了多线程问题,又没有同步损失。

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

private Singleton() {

}

private static final Singleton instance = new Singleton();

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

这样虽然解决了同步损失,但是,单例的构建很可能是一个耗时操作;

它并不是一个懒加载操作,同时,如果单例的构建需要外部参数的话,这个方法就用不上了。

5. 双检锁单例

这种单例实现是目前用的比较多的形式;

既然单例的多线程同步只需要在对象构建时进行,那么,我们就可以通过对 instance 进行两次检查。

1
2
3
4
5
6
7
8
9
10
11
12
13
private static Singleton getInstance() {
// 检查对象是否已经构建
if (instance == null) {
synchronized(Singleton.class) {
// 防止两个线程同时进入第一个 if 造成的对象重复构建
if (instance == null) {
instance = new Singleton();
}
}
}

return instance;
}

这里着重解释第二个判断:

当两个线程通过第一个 if 进入同步区后;

线程一先获取锁,进行对象构建;

线程一完毕后,线程二进入同步区;

如果此时没有第二个 if,那么对象就会进行重复构建。

当对象构建完毕后,外部的 if 将会跳过,不会再进行同步过程;

这样就解决了同步的性能损失。

看起来这个实现已经很完美了,但是还是有问题;

问题就在于,instance = new Singleton() 不是一个原子操作

这个操作分为三步:

  1. instance 分配内存
  2. 调用 Singleton 的构造函数进行对象构造
  3. instance 指向分配的内存空间

而在 JIT 即时编译优化中,会出现指令重排;

最终的执行顺序很可能不是 1-2-3 而是 1-3-2;

那么在 3 完成后,线程二抢占锁,此时 instance 不为空,于是线程二返回,报错。

一个改进则是对于 instance 变量采用 volatile 进行修饰,防止指令重排;

volatile 变量在赋值操作后会存在 内存屏障,防止读操作在赋值操作之前进行。

所以,双检锁单例的正确实现形式如下:

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

private Singleton() {

}

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

return instance;
}
}

6. 静态内部类

可以看到,上面的双检锁单例的实现过于繁琐;

有没有一种既线程安全,又采用懒加载而且实现简单的实现方法呢?

有的,我们可以采用静态内部类来实现单例。

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

}

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

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

首先看到,我们把单例的实例放入了它的静态内部类中;

这种实现方法的难度比双检锁单例下降了许多;

其原理在于,我们不再 自行实现 线程安全;

通过将实例交给静态内部类,我们可以让 JVM 保证天生的线程安全。

Singleton 被加载时,其内部类并不会初始化;

而当 getInstance() 调用时,其内部类被 加载 入内存中;

JVM 保证所有对类的读写操作均在类加载 之后 进行;

这样,就保证了线程安全;

同时,由于 getInstance() 不是同步方法,也不会有同步损失。

7. 序列化和反射攻击

那么,我们的单例实现是否已经没有问题了呢?

并不是!

由于我们都是通过设定 访问修饰符 来达到将构造函数封装的目的;

但是,当其他使用者使用 反射 来进行对象构建时,单例模式就会被破坏了!

1
2
Constructor<?>[] cons = getDeclaredConstructors();
cons. setAccessible(true);

其中,一个很常见的问题就是单例的序列化;

当我们采用 Serializable 接口时;

对象在序列化和反序列化的过程中,会使用 反射 调用无参构造方法进行对象构造。

那么,我们该如何防止对象被反射攻击呢?

一种方法是采用 flag,如果对象被反射攻击,那么就抛出异常;

1
2
3
4
5
6
7
8
9
10
11
12
public class Singleton {
private static boolean flag = false;

private Singleton() {
if (Singleton.flag == false) {
Singleton.flag = true;
}
else {
throw new RuntimeException("Reflect Attack!");
}
}
}

而对于序列化造成的对象的重复构建,我们可以采用重载 readResolve() 的方法进行。

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

8. 枚举实现(Best Practice)

可以看到,当我们把所有问题都考虑到之后,单例的实现已经变得非常复杂了。

所以有没有一种更为简单的方法能满足上面的所有要求呢?

这就是枚举实现:

1
2
3
4
5
6
7
public enum Singleton {
INSTANCE;

public void doSomeThing() {
// operations
}
}

使用:

1
2
3
public static void main(String[] args) {
Singleton.INSTANCE.doSomeThing();
}

它满足:

  1. 线程安全
  2. 无性能损失
  3. 防反射攻击
  4. 防止序列化重复构建

而且是最为简单的一种单例实现方法。

0%