单例模式-Singleton Pattern

引言

单例模式,顾名思义就是在程序运行期间,一个类只有一个实例。

使用场景:需要在系统中确保类只有一个实例,一般这种类的创建都会比较占用系统资源。比如配置文件初始化,将配置文件中的数据读取到类中,通常需要耗费一定的系统资源,而且配置文件中的内容一般都是不变的,修改完配置文件一般都会要求重启系统。所以这种类最适合使用单例模式。

定义

Ensure a class only has one instance, and provide a global point of access to it.

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

简单单例模式

实现

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/**
* 简单的单例模式
*
* @author jaune
* @since 1.0.0
*/
public class SimpleConfigUtils {

private static org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(SimpleConfigUtils.class);
private static SimpleConfigUtils instance = null;

private Map<String, String> props = new HashMap<>();

/**
* 单例的目的就是为了控制实例数量,所以构造函数一定要私有化,这样才能被其他使用者不能够随便实例化对象。
*/
private SimpleConfigUtils() {
this.readFromConfigFile();
logger.info("对象实例化完成。");
}

/**
* 获取配置实例
* @return 配置实例
*/
public static SimpleConfigUtils getInstance() {
if (instance == null) {
instance = new SimpleConfigUtils();
}
return instance;
}

/**
* 根据配置项的名称获取配置项的值
* @param propName 配置项的名称
* @return 配置项的值
*/
public String getPropertyValue(String propName) {
return this.props.get(propName);
}

private void readFromConfigFile() {
logger.info("假装从配置文件中读取了配置");
props.put("application.name", "DesignPattern");
props.put("application.type", "SpringBoot");
}
}

实现单例模式要注意以下要点

  • 构造函数私有化
  • 提供一个静态方法来获取对象
  • 对象的实例保存在静态变量中。获取实例的时候要验证变量是否被初始化。

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SimpleConfigUtilsTest {
private static Logger logger = LoggerFactory.getLogger(SimpleConfigUtilsTest.class);

@Test
public void test() {
SimpleConfigUtils configUtils = SimpleConfigUtils.getInstance();
String applicationName = configUtils.getPropertyValue("application.name");
logger.info("application.name: {}", applicationName);

SimpleConfigUtils configUtils2 = SimpleConfigUtils.getInstance();
String applicationType = configUtils2.getPropertyValue("application.type");
logger.info("application.type: {}", applicationType);
}

}

控制台输出

1
2
3
4
19-02-19 17:21:40 INFO  com.codestd.singleton.SimpleConfigUtils - 假装从配置文件中读取了配置
19-02-19 17:21:40 INFO com.codestd.singleton.SimpleConfigUtils - 对象实例化完成。
19-02-19 17:21:40 INFO com.codestd.singleton.SimpleConfigUtilsTest - application.name: DesignPattern
19-02-19 17:21:40 INFO com.codestd.singleton.SimpleConfigUtilsTest - application.type: SpringBoot

从控制台可以看出类只被实例化了一次。

问题

下面我们模拟多线程环境再进行一次测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class SimpleConfigUtilsTest {
private static Logger logger = LoggerFactory.getLogger(SimpleConfigUtilsTest.class);

private CountDownLatch countDownLatch = new CountDownLatch(4);

@Test
public void testMultiThreading() throws InterruptedException {
for (int i = 0; i < 4; i++) {
new Thread(() -> {
SimpleConfigUtils configUtils = SimpleConfigUtils.getInstance();
String applicationName = configUtils.getPropertyValue("application.name");
String applicationType = configUtils.getPropertyValue("application.type");
logger.info("{} - application.name: {}", Thread.currentThread().getName(), applicationName);
logger.info("{} - application.type: {}", Thread.currentThread().getName(), applicationType);
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
}

}

控制台打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
19-02-19 18:07:53 INFO  com.codestd.singleton.SimpleConfigUtils - 假装从配置文件中读取了配置
19-02-19 18:07:53 INFO com.codestd.singleton.SimpleConfigUtils - 假装从配置文件中读取了配置
19-02-19 18:07:53 INFO com.codestd.singleton.SimpleConfigUtils - 对象实例化完成。
19-02-19 18:07:53 INFO com.codestd.singleton.SimpleConfigUtils - 假装从配置文件中读取了配置
19-02-19 18:07:53 INFO com.codestd.singleton.SimpleConfigUtils - 对象实例化完成。
19-02-19 18:07:53 INFO com.codestd.singleton.SimpleConfigUtils - 假装从配置文件中读取了配置
19-02-19 18:07:53 INFO com.codestd.singleton.SimpleConfigUtils - 对象实例化完成。
19-02-19 18:07:53 INFO com.codestd.singleton.SimpleConfigUtils - 对象实例化完成。
19-02-19 18:07:53 INFO com.codestd.singleton.SimpleConfigUtilsTest - Thread-0 - application.name: DesignPattern
19-02-19 18:07:53 INFO com.codestd.singleton.SimpleConfigUtilsTest - Thread-1 - application.name: DesignPattern
19-02-19 18:07:53 INFO com.codestd.singleton.SimpleConfigUtilsTest - Thread-1 - application.type: SpringBoot
19-02-19 18:07:53 INFO com.codestd.singleton.SimpleConfigUtilsTest - Thread-3 - application.name: DesignPattern
19-02-19 18:07:53 INFO com.codestd.singleton.SimpleConfigUtilsTest - Thread-2 - application.name: DesignPattern
19-02-19 18:07:53 INFO com.codestd.singleton.SimpleConfigUtilsTest - Thread-2 - application.type: SpringBoot
19-02-19 18:07:53 INFO com.codestd.singleton.SimpleConfigUtilsTest - Thread-3 - application.type: SpringBoot
19-02-19 18:07:53 INFO com.codestd.singleton.SimpleConfigUtilsTest - Thread-0 - application.type: SpringBoot

在多线程环境下,这种方式居然失效了。我们在后文中分析如何保证在多线程环境下,能够只有一个实例。也就是单例模式在多线程环境有效。

懒汉模式

利用方法锁实现

一种方式是在获取实例的方法上加锁

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
31
32
33
34
35
36
37
38
39
40
41
42
43
// 代码清单1

public class LazyInstantiationConfigUtils {

private static Logger logger = LoggerFactory.getLogger(SimpleConfigUtils.class);
private static LazyInstantiationConfigUtils instance = null;

private Map<String, String> props = new HashMap<>();

/**
* 单例的目的就是为了控制实例数量,所以构造函数一定要私有化,这样才能被其他使用者不能够随便实例化对象。
*/
private LazyInstantiationConfigUtils() {
this.readFromConfigFile();
logger.info("对象实例化完成。");
}

/**
* 获取配置实例,方法上加锁。当多个线程同时调用时只有一个线程可以访问方法,其他线程将被阻塞。
* @return 配置实例
*/
public static synchronized LazyInstantiationConfigUtils getInstance() {
if (instance == null) {
instance = new LazyInstantiationConfigUtils();
}
return instance;
}

/**
* 根据配置项的名称获取配置项的值
* @param propName 配置项的名称
* @return 配置项的值
*/
public String getPropertyValue(String propName) {
return this.props.get(propName);
}

private void readFromConfigFile() {
logger.info("假装从配置文件中读取了配置");
props.put("application.name", "DesignPattern");
props.put("application.type", "SpringBoot");
}
}

这种方式简单,但是性能差。每次调用都会判断是否有锁,在同一时刻只能有一个线程调用。

双重检查锁定(Double-Check Locking)

另外一种方式是,使用双重检查锁定(Double-Check Locking)或者叫双重锁定机制(double locking mechanism)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 代码清单2

/**
* 获取配置实例
* @return 配置实例
*/
public static LazyInstantiationConfigUtils getInstance() {
if (instance == null) {
synchronized (LazyInstantiationConfigUtils.class) { // (1)
if (instance == null) { // (2)
instance = new LazyInstantiationConfigUtils(); // (3)
}

}
}
return instance;
}

指令重排问题(Out-of-Order)

问题就在于(3),在构造函数执行之前,变量instance变为了non-null

想象有两个线程,执行顺序如下

  1. Thread 1 进入getInstance()方法
  2. Thread 1 进入同步块,也就是(1),因为这是instancenull
  3. Thread 1 执行了代码(3),但是构造函数还没有执行。这时instance变为了non-null
  4. Thread 2 抢占了 Thread 1的资源,开始执行
  5. Thread 2 进入getInstance()方法
  6. Thread 2 判断instance不为null,并把为初始化完成的instance返回

问题已经出现了, Thread 2获取到了一个未初始化的对象。那么在使用的时候肯定也是有问题的。如何解决这个问题呢?

第一种方式是,使用两段双重锁定机制,如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 获取配置实例
* @return 配置实例
*/
public static LazyInstantiationConfigUtils getInstance() {
if (instance == null) {
synchronized (LazyInstantiationConfigUtils.class) {
LazyInstantiationConfigUtils inst = instance;
if (inst == null) {
synchronized (LazyInstantiationConfigUtils.class) {
inst = new LazyInstantiationConfigUtils();
}
instance = inst;
}
}
}
return instance;
}

这种方式可以保证在构造函数未执行完之前,instance一直是null

另一种方法是给instance变量增加volatile修饰符。使用volatile可以保证执行顺序的正确性,保证变量的读在写之后。但是也会带来新的问题。

  • volatile会阻止指令重拍,这样就会屏蔽掉JVM所做的优化,降低了程序的执行效率。
  • 许多虚拟机并没有实现volatile的顺序一致性(Sequential Consistency)。比如在1.5以下的版本中。

饿汉模式

饿汉模式就是在定义静态变量时就实例化对象,因此在类加载的时候就完成了对象的实例化,而并不是在类第一次使用时。

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

private static EarlyInstantiationConfigUtils instance = new EarlyInstantiationConfigUtils();

private EarlyInstantiationConfigUtils() {
// 初始化过程
}

/**
* 获取配置实例
* @return 配置实例
*/
public static EarlyInstantiationConfigUtils getInstance() {
return instance;
}
}

这种方式是最简单的方式,但是会造成一定的资源浪费。因为无论你使用与否,JVM都会在类加载的时候完成类的实例化。

Initialization on Demand Holder (IoDH)

这种方式可以实现实例的懒加载。

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

private EarlyInstantiationConfigUtils() {
// 初始化过程
}

private static class DemandHolder {
private static EarlyInstantiationConfigUtils instance = new EarlyInstantiationConfigUtils();
}

/**
* 获取配置实例
* @return 配置实例
*/
public static EarlyInstantiationConfigUtils getInstance() {
return DemandHolder.instance;
}
}

在类加载时,由于instance不是类的静态成员变量,所以不会初始化。在调用getInstance()方法时,加载DemandHolder类,这时instance作为静态成员变量,开始初始化。这是由Java虚拟机来保证线程安全的,并确保只有一个实例被创建。

通过使用IoDH,我们既可以实现延迟加载,又可以保证线程安全,不影响系统性能。

参考资料

作者

大扑棱蛾子(jaune162@126.com)

发布于

2024-02-14

更新于

2024-09-11

许可协议

评论