leisurexi's Blog.

单例模式

字数统计: 2k阅读时长: 7 min
2019/05/26 Share

单例模式(Singletom Pattern),保证一个类仅有一个实例,并提供一个访问它的全局访问点。

模式定义

通常我们可以让一个全局变量使得一个对象被访问,但它不能防止你实例化多个对象。一个最好的办法就是,让类自身负责保存它的唯一实例。这个类可以保证没有其他实例可以被创建,并且它可以提供一个访问该实例的方法。

模式结构

Singleton类,定义一个getInstance操作,允许客户端访问它的唯一实例。getInstance是一个静态方法,主要负责创建自己的唯一实例。

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

private static Singleton instance;

//私有构造方法,不让外加利用new创建实例
private Singleton() {
}

//此方法是获得本类实例的唯一全局访问点
public static Singleton getInstance() {
//若实例不存在,创建一个新的实例,否则返回已有实例
if (instance == null) {
instance = new Singleton();
}
return instance;
}

}

然后用客户端程序试一下,是否多次调用得到的还是一样的实例。

1
2
3
4
5
6
7
public static void main(String[] args) {
Singleton instance1 = Singleton.getInstance();
Singleton instance2 = Singleton.getInstance();
if (instance1 == instance2) {
System.out.println("俩个对象是相同的实例");
}
}

运行结果

1
俩个对象是相同的实例

模式实现

懒汉式,线程不安全

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

private static Singleton instance;

//私有构造方法,不让外加利用new创建实例
private Singleton() {
}

//此方法是获得本类实例的唯一全局访问点
public static Singleton getInstance() {
//若实例不存在,创建一个新的实例,否则返回已有实例
if (instance == null) {
instance = new Singleton();
}
return instance;
}

}

这段代码和上面的示例一样,简单明了,而且使用了懒加载,但却有一个致命的问题,就是线程不安全,也就是说在多线程环境下可能会创建出多个实例。

懒汉式,线程安全

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

private static Singleton instance;

//私有构造方法,不让外加利用new创建实例
private Singleton() {
}

//此方法是获得本类实例的唯一全局访问点
public synchronized static Singleton getInstance() {
//若实例不存在,创建一个新的实例,否则返回已有实例
if (instance == null) {
instance = new Singleton();
}
return instance;
}

}

这个只是在getInstance方法上加了synchronized关键字,解决了线程不安全的问题,但是并不高效。因为在任何时候只有一个线程能调用getInstance方法。但是同步操作只需要第一次调用时才需要,所以有了双重检验锁。

双重检验锁

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

private static Singleton instance;

//私有构造方法,不让外加利用new创建实例
private Singleton() {
}

//此方法是获得本类实例的唯一全局访问点
public static Singleton getInstance() {
//若实例不存在,创建一个新的实例,否则返回已有实例
if (instance == null) { //single checked
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); //double checked
}
}
}
return instance;
}

}

双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法。程序员称其为双重检查锁,因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。

上面的代码还是会存在问题的,只要在于instance = new Singleton(),这并非是一个原子操作,事实上在JVM中这句话大概做了下面3件事情。

  1. 给instance分配内存。
  2. 调用Singleton的构造函数来初始化成员变量。
  3. 将instance对象指向分配的内存空间(执行完这步instance就是非null了)。

但是在JVM的即时编译中存在指令重排。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是1-3-2,也可能是1-2-3。如果是后者,则在3执行完毕、2未执行之前,被线程二抢占了,这是instance已经是非null了(但却没有初始化),所以线程二回直接返回instance,然后使用,然后顺理成章地报错。

解决方法就是讲instance变量声明成volatile就可以了。

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

private volatile static Singleton instance;

//私有构造方法,不让外加利用new创建实例
private Singleton() {
}

//此方法是获得本类实例的唯一全局访问点
public static Singleton getInstance() {
//若实例不存在,创建一个新的实例,否则返回已有实例
if (instance == null) { //single checked
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton(); //double checked
}
}
}
return instance;
}

}

有些人认为使用volatile的原因是可见性,也就是可以保证线程在本地不会存有instance的副本,每次都是去主内存中读取。但其实是不对的。使用volatile的主要原因是其另一个特性:禁止指令重排序优化。也就是说,在volatile变量的赋值操作后会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障前。比如上面的例子,取操作必需在执行完1-2-3或者1-3-2之后,不存在执行到1-3然后取到值得情况。从「先行发生原则」的角度理解的话,就是对于一个volatile变量的写操作都先行发生于后面对这个变量的读操作。(这里的 “后面” 是时间上的顺序)

但是特别注意在 Java 5 以前的版本使用了 volatile 的双检锁还是有问题的。其原因是 Java 5 以前的 JMM (Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用 volatile。

饿汉式

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

public static Singleton instance = new Singleton();

private Singleton() {
}

public static Singleton getInstance() {
return instance;
}

}

它基于classloader机制避免了多线程的问题,是线程安全的,但容易产生垃圾对象;但是没有加锁,执行效率会高;最大的缺点就是类加载时就初始化,浪费内存。

静态内部类

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

private Singleton() {
}

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

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

}

这种方式利用了classloader机制来保证初始化instance时只有一个线程,它跟上面饿汉式不同的是:第三种方式只要Singleton类被加载了,那么INSTANCE就会被实例化(没有达到懒加载的效果),而这种方式是Singleton类被加载了,INSTANCE不一定被初始化。因为SingletonHolder没有被主动使用,只要通过显示调用getInstance()方法时,才会显示装在SingletonHolder类,从而实例化INSTANCE

枚举

1
2
3
public enum Singleton {
INSTANCE;
}

这种方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,支持自动序列化机制,绝对防止多次序列化。创建枚举默认就是线程安全的,所以不需要担心多线程的问题。

总结

一般情况下直接使用饿汉式就好了,如果明确要求懒加载会更倾向于使用静态内部类,如果涉及到反序列化创建对象时可以试着用枚举的方式来实现单例。

CATALOG
  1. 1. 模式定义
  2. 2. 模式结构
  3. 3. 模式实现
    1. 3.1. 懒汉式,线程不安全
    2. 3.2. 懒汉式,线程安全
    3. 3.3. 双重检验锁
  4. 4. 饿汉式
  5. 5. 静态内部类
  6. 6. 枚举
  7. 7. 总结