0%

单例模式

Preface

单例模式为GOF设计模式中的一种,当我们创建的对象需要昂贵的资源,且该对象又被频繁使用,同时,这个对象的状态是共享的,为了避免资源的浪费以及加载资源的时间,我们往往会引入单例模式。因此,要实现单例模式,其核心就是要在已有该类的实例前提下,阻止其他开发人员再创建类的新实例

在Java语言中,通常提供了如下几种单例模式的实现方式。

懒汉模式

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SingletonDemo {
private static SingletonDemo instance;

private SingletonDemo() {
}

public static SingletonDemo getInstance() {
if (instance == null) {
instance = new SingletonDemo();
}
return instance;
}
}

通过提供一个静态的对象 instance,利用 private 权限的构造方法和 getInstance() 方法来给予访问者一个单例。缺点是,没有考虑到线程安全,可能存在多个访问者同时访问,并同时构造了多个对象的问题。之所以叫做懒汉模式,主要是因为此种方法可以非常明显的 lazy loading。针对懒汉模式线程不安全的问题,我们自然想到了,在 getInstance() 方法前加锁,于是就有了第二种实现。

线程安全的懒汉模式

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SingletonDemo {
private static SingletonDemo instance;

private SingletonDemo() {
}

public static synchronized SingletonDemo getInstance() {
if (instance == null) {
instance = new SingletonDemo();
}
return instance;
}
}

然而并发其实是一种特殊情况,大多时候这个锁占用的额外资源都浪费了,这种打补丁方式写出来的结构效率很低。

饿汉模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SingletonDemo {
private static SingletonDemo instance = new SingletonDemo();

private SingletonDemo() {
System.out.println("Singleton has loaded");
}

public static SingletonDemo getInstance() {
return instance;
}

public static void printHello() {
System.out.println("hello");
}
}

由于 instance 成员变量使用 static 修饰,在 JVM 首次加载该类时单例对象 instance 会被创建并常驻内存,所以是线程安全的。但显然,这种方法没有起到 lazy loading 的效果。举个例子,也许你只想调用 SingletonDemo.printHello() 方法,但同时打印了 ”Singleton has loaded“,这可能是你不想见到的。

所以我们引入静态类内部加载的方式,也算是单例模式较为完善的解决方案。

静态类内部加载

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

public static SingletonDemo getInstance() {
return SingletonHolder.instance;
}

private static class SingletonHolder {
private static SingletonDemo instance = new SingletonDemo();
}
}

使用私有内部类的好处是,静态内部类不会在单例加载时就加载,而是在调用 getInstance() 方法时才进行加载。且单例的创建是在 SingletonHolder 被 JVM 加载时完成,故而也是线程安全的。因此该方法兼备了以上两种实现的优点。

双重校验锁法

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

private SingletonDemo() {
System.out.println("Singletonhasloaded");
}

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

接下来我解释一下在并发时,双重校验锁法会有怎样的情景:

  • STEP 1. 线程A访问 getInstance() 方法,因为单例还没有实例化,所以进入了锁定块。
  • STEP 2. 线程B访问 getInstance() 方法,因为单例还没有实例化,得以访问接下来代码块,而接下来代码块已经被线程1锁定。
  • STEP 3. 线程A进入下一判断,因为单例还没有实例化,所以进行单例实例化,成功实例化后退出代码块,解除锁定。
  • STEP 4. 线程B进入接下来代码块,锁定线程,进入下一判断,因为已经实例化,退出代码块,解除锁定。
  • STEP 5. 线程A初始化并获取到了单例实例并返回,线程B获取了在线程A中初始化的单例。

理论上双重校验锁法是线程安全的,并且,这种方法实现了lazy loading。