0%

单例模式

概述

单例模式:某一个类在系统中只需要有一个实例对象,而且对象是由这个类自行实例化并提供给系统其它地方使用,这个类称为单例类。单例模式是 GOF 23 种设计模式中最简单的一种,但同时也是在项目中接触最多的一种。单例模式属于一种创建型设计模式。

使用场景

大家都使用过 Windows 任务管理器,正常情况下,无论我们在 Windows 任务栏的右键菜单上点击启动多少次“任务管理器”,系统始终只能弹出一个任务管理器窗口。也就是说,在一个 Windows 系统中,系统只维护一个任务管理器。这就是一个典型的单例模式运用。
再举一个例子,网站的计数器,一般也是采用单例模式实现,如果你存在多个计数器,每一个用户的访问都刷新计数器的值,这样的话你的实计数的值是难以同步的。但是如果采用单例模式实现就不会存在这样的问题,而且还可以避免线程安全问题。同样多线程的线程池的设计一般也是采用单例模式,这是由于线程池需要方便对池中的线程进行控制。
可以看出,我们在程序中使用单例模式,目的一般是处理资源访问的冲突,或者从业务概念上,有些数据在系统中只应保存一份,那也比较适合设计为单例类,比如配置类、全局流水号生成器等。

UML 类图

image.png

实现要点

单例模式虽然简单,但是要写出一个能保证在多线程环境下也能保证实例唯一性的单例确不是那么简单,实现一个正确的单例模式有以下几个要点: 1.某个类只能有一个实例,即使是多线程运行环境下; 2.单例类的实例一定是单例类自身创建,而不是在单例类外部用其它方式如 new 方式创建; 3.单例类需要提供一个方法向整个系统提供这个实例对象。

两种模式

单例模式分为饿汉模式和懒汉模式,这两种模式很好理解,懒汉模式的意思就是这个类很懒,只要别人不找它要实例,它都懒得创建。饿汉模式在初始化时,我们就创建了唯一的实例,即便这个实例后面并不会被使用。
下面分别介绍两种单例模式的写法。

懒汉式

下面这种写法的单例是大家最简单最容易写出的一种单例写法,只适用于单线程的系统,也就是说它不是线程安全的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//懒汉式,线程不安全
class Singleton1{
private static Singleton1 instance;

//构造函数定义为私有,防止外部创建实例
private Singleton1(){

}

//系统使用单例的入口
public static Singleton1 getInstance(){
if (null == instance){
instance = new Singleton1();
}

return instance;
}
}

针对线程不安全的问题,可以通过获取实例的方法添加了 synchronized 来解决,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//懒汉式,线程安全,效率低
class Singleton2{
private static Singleton2 instance;

//构造函数定义为私有,防止外部创建实例
private Singleton2(){

}

//系统使用单例的入口
public static synchronized Singleton2 getInstance(){
if (null == instance){
instance = new Singleton2();
}

return instance;
}
}

这样一来,确实线程安全了,但是又带来了另一个问题:程序的性能极大的降低了,高并发下多个线程去获取这个实例,现在却要排队。
针对性能问题,有同学想到了减小 synchronized 的粒度,不加在方法上,而是放在代码块中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//懒汉式,线程不安全
class Singleton3{
private static Singleton3 instance;

//构造函数定义为私有,防止外部创建实例
private Singleton3(){

}

//系统使用单例的入口
public static Singleton3 getInstance(){
if (null == instance){
synchronized(Singleton3.class) {
instance = new Singleton3();
}
}

return instance;
}
}

但是,很不幸,如果改成这样,又变得线程不安全了,我们试着分析一个代码执行的场景:假设我们有两个线程 T1 与 T2 并发访问 getInstance 方法。当 T1 执行完 if (instance == null)且 instance 为 null 时,其 CUP 执行时间被 T2 抢占,所以 T1 还没有创建实例。T2 也执行 if (instance == null),此时 instance 肯定还为 null,T2 执行创建实例的代码,当 T1 再次获得 CPU 执行时间后,其从 synchronized 处恢复,又会创建一个实例。
那么有没有一种写法,可以同时兼顾到效率和线程安全两方面了,还真有,就是我们下面将要介绍的 double-check 的方式。

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
////懒汉式,线程安全,效率还可以
class Singleton4{
//注意加上volatile关键字
private static volatile Singleton4 instance;

//构造函数定义为私有,防止外部创建实例
private Singleton4(){

}

//系统使用单例的入口
public static Singleton4 getInstance(){
//第一次检查提高访问性能
if (null == instance){
synchronized(Singleton4.class) {
//第二次检查为了线程安全
if(instance ==null) {
instance = new Singleton4();
}
}
}

return instance;
}
}

这种单例的写法做了两次 if (null == instance)的判断,因此被称为 double-check 的方式。

  • 第一次 check 为了提高访问性能。因为一旦实例被创建,后面线程的所有的 check 都为假,不需要执行 synchronized 竞争锁了。
  • 第二次 check 是为了线程安全,确保多线程环境下只生成一个实例。

需要注意的是,这种方式,在定义实例时一定需要加上 volatile 关键字,禁止虚拟机指令重排,否则,还是有一定几率会生成多个实例,关于 volatile 关键字和指令重排的问题这里不过多介绍,后面在多线程安全系列文章中再详细介绍。

饿汉式

使用静态常量在类加载时候就创建了实例,属于饿汉模式。其是线程安全的,这一点由 JVM 来保证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//饿汉式,线程安全
class Singleton5{
//
private static final Singleton5 INSTANCE = new Singleton5();

//构造函数定义为私有,防止外部创建实例
private Singleton5(){

}

//系统使用单例的入口
public static Singleton5 getInstance(){
return INSTANCE;
}
}

区别

  1. 线程安全:饿汉式在线程还没出现之前就已经实例化了,所以饿汉式一定是线程安全的。懒汉式加载是在使用时才会去 new 实例的,那么你去 new 的时候是一个动态的过程,是放到方法中实现的,如果这个时候有多个线程访问这个实例,这个时候实例还不存在,还在 new,就会进入到方法中,有多少线程就会 new 出多少个实例。一个方法只能 return 一个实例,那最终 return 出哪个呢?是不是会覆盖很多 new 的实例?这种情况当然也可以解决,那就是加同步锁,避免这种情况发生。
  2. 执行效率:饿汉式没有加任何的锁,因此执行效率比较高。懒汉式一般使用都会加同步锁,效率比饿汉式差。
  3. 内存使用:饿汉式在一开始类加载的时候就实例化,无论使用与否,都会实例化,所以会占据空间,浪费内存。懒汉式什么时候用就什么时候实例化,不浪费内存。