引言
单例模式,顾名思义就是在程序运行期间,一个类只有一个实例。
使用场景:需要在系统中确保类只有一个实例,一般这种类的创建都会比较占用系统资源。比如配置文件初始化,将配置文件中的数据读取到类中,通常需要耗费一定的系统资源,而且配置文件中的内容一般都是不变的,修改完配置文件一般都会要求重启系统。所以这种类最适合使用单例模式。
定义
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
|
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("对象实例化完成。"); }
public static SimpleConfigUtils getInstance() { if (instance == null) { instance = new SimpleConfigUtils(); } return instance; }
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
|
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("对象实例化完成。"); }
public static synchronized LazyInstantiationConfigUtils getInstance() { if (instance == null) { instance = new LazyInstantiationConfigUtils(); } return instance; }
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
|
public static LazyInstantiationConfigUtils getInstance() { if (instance == null) { synchronized (LazyInstantiationConfigUtils.class) { if (instance == null) { instance = new LazyInstantiationConfigUtils(); } } } return instance; }
|
指令重排问题(Out-of-Order)
问题就在于(3)
,在构造函数执行之前,变量instance
变为了non-null
。
想象有两个线程,执行顺序如下
- Thread 1 进入
getInstance()
方法
- Thread 1 进入同步块,也就是
(1)
,因为这是instance
是null
- Thread 1 执行了代码
(3)
,但是构造函数还没有执行。这时instance
变为了non-null
- Thread 2 抢占了 Thread 1的资源,开始执行
- Thread 2 进入
getInstance()
方法
- 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
|
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() { }
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(); }
public static EarlyInstantiationConfigUtils getInstance() { return DemandHolder.instance; } }
|
在类加载时,由于instance
不是类的静态成员变量,所以不会初始化。在调用getInstance()
方法时,加载DemandHolder
类,这时instance
作为静态成员变量,开始初始化。这是由Java虚拟机来保证线程安全的,并确保只有一个实例被创建。
通过使用IoDH,我们既可以实现延迟加载,又可以保证线程安全,不影响系统性能。
参考资料