设计模式之单例模式

单例模式定义

单例模式是设计模式的一种,为了保证一个类仅有一个实例,并提供一个访问它的全局访问点。

特点
  • 单例类保证只有一个实例(构造函数私有化)
  • 单例类自己创建自己
  • 单例类必须为其他对象提供访问唯一实例的方法
应用
  • 配置类信息
  • Java中的Runtime类
  • 线程池的设计一般也是采用单例模式

单例模式的实现有饿汉模式、懒汉模式(多种)、静态内部类、枚举类等多种实现方式。

饿汉模式

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

通过static的静态初始化方式,在该类第一次被加载的时候,就有一个Singleton类的实例被创建出来了。这样就保证在第一次想要使用该对象时,它已经被初始化好了。

注:JVM在初始化一个类的时候(即调用类构造函数())会自动同步,因此不用关心线程安全问题,但是一旦完成类加载过程,无论是否使用该单例,该单例都已经实际占用内存。

优点: 线程安全
缺点:占用内存(只要类加载无论是否使用单例,都会占用内存。万一单例对象很大)

懒汉模式

线程不安全的懒汉模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//懒汉
public class SingletonLazy {
private SingletonLazy() {};
private static SingletonLazy instance = null;
//线程不安全
public static SingletonLazy getInstance() {
if (instance == null) {
return new SingletonLazy();
}
return instance;
}
}

在该对象真正被使用的时候才会实例化。即调用getInstance()方法时。但是在这种形式下会存在线程安全问题(多线程同时进入if语句)。

线程安全的懒汉模式
1
2
3
4
5
6
public synchronized static SingletonLazy getInstance2() {
if (instance == null) {
return new SingletonLazy();
}
return instance;
}

这种实现方式虽然可以解决线程安全问题,但是效率很低,因为synchronized的加锁范围是整个方法,导致不必要的开销。

双重校验
1
2
3
4
5
6
7
8
9
10
public static SingletonLazy getInstance3() {
if (instance == null) {
synchronized (SingletonLazy.class) {
if (instance == null) {
return new SingletonLazy();
}
}
}
return instance;
}

采用 synchronized 同步代码块代替同步锁的方式,缩小了锁的范围,提高效率。
但是这种实现方式再多线程访问时,存在一个由指令重排序引起的问题。

指令重排序引起的问题

具体如下:instance = new Singleton()不是原子操作,实际上被拆分为了三步:
1) 分配内存;
2) 初始化对象;
3) 将instance指向分配的对象内存地址。

1
2
3
4
5
6
7
8
9
10
正常对象初始化顺序:
memory = allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance = memory; //3:设置instance指向刚分配的内存地址
重排序之后:
memory = allocate(); //1:分配对象的内存空间
instance = memory; //3:设置instance指向刚分配的内存地址
//注意,此时对象还没有被初始化!
ctorInstance(memory); //2:初始化对象

试想一下,当一个线程访问获取单例对象时指令进行了重排序,instance先指向了分配的对象内存地址,但还未初始化完成。此时又有一个线程访问,在判断为null的时候发现不为空,于是就直接返回了,但实际上这个对象还未初始化完成,于是就会出错。
如何解决?Volatile关键字。

双重校验优化版(volatile)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SingletonDoubleCheck {
private SingletonDoubleCheck() {};
private static volatile SingletonDoubleCheck instance = null;
public SingletonDoubleCheck getInstance() {
if (instance == null) { //Check 1
synchronized (SingletonDoubleCheck.class) {
if (instance == null) { //Check 2
instance = new SingletonDoubleCheck();
}
}
}
return instance;
}
}

使用volatile关键字的目的不是保证可见性(synchronized已经保证了可见性),而是为了保证有序性(防止指令重排序)。

静态内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SingletonHolder {
//私有类,只能在本类访问
private static class Holder {
private static final SingletonHolder instance = new SingletonHolder();
public Holder(){};
static {
System.out.println("内部类被初始化了");
}
}
private SingletonHolder() {};
public SingletonHolder getInstance() {
return Holder.instance;
}
//测试
public static void main(String[] args) {
SingletonHolder singletonHolder = new SingletonHolder(); //单独new对象,不会加载Holder,不会输出语句
singletonHolder.getInstance(); //调用方法时才会加载Holder,输出语句
}
}

优点:

  1. 线程安全(静态变量)
  2. 可实现懒加载

为什么静态内部类单例可以实现延迟加载?

原理:加载一个类时,其内部类不会同时被加载。一个类被加载,当且仅当其某个静态成员(静态域、构造器、静态方法等)被调用时发生。
所以当外部类被加载时,内部类并没有被加载,内部类不被加载则不需要进行类初始化,所以单例对象未初始化。

枚举

1
2
3
public enum SingletonEnum {
SINGLETON;
}

优点: 线程安全
缺点: 占用内存,无法实现懒加载。

如何破坏单例模式?

通过反射或序列化,我们仍然能够访问到私有构造器,创建新的实例破坏单例模式。此时,只有枚举模式能天然防范这一问题。