单例模式

单例模式

一、什么是单例模式?

单例设计模式(Singleton Design Pattern):指一个类只有一个实例,且该类能自行创建这个实例的一种模式。

这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。

单例模式有 3 个特点:

  1. 单例类只有一个实例对象
  2. 该单例对象必须由单例类自行创建
  3. 单例类对外提供一个访问该单例的全局访问点

二、为什么要使用单例?

从业务概念上,有些数据在系统中只应该保存一份,就比较适合设计为单例类。

  • 表示全局唯一类,比如系统配置类
  • 处理资源访问冲突,比如打印日志类

三、如何实现一个单例?

单例需要考虑以下几点:

  • 单例的构造函数必须是 private 访问权限的,避免外部创建对象
  • 考虑线程并发创建单例对象的情况,多线程同时创建单例对象时,是否能够保证只有一个单例生成
  • 考虑是否要延迟加载的情况,比如单例对象是否要等到第一次获取的时候才生成,还是一开始就存在
  • 考虑获取单例对象的性能问题,比如对方法加锁会导致性能变差

单例创建的几种方式:

3.1 饿汉式

  • 在类加载期间,就已经把单例对象初始化好了
  • 不存在线程安全问题
  • 缺点是不支持延迟加载,没有用到该单例就已经初始化好对象了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 饿汉单例模式
*/
public class HungrySingleton {

// 类加载的时候初始化,因此不需要同步
private static final HungrySingleton instance = new HungrySingleton();

// 避免在外部被实例化
private HungrySingleton(){}

/**
* 直接获取单例
* @return 单例对象
*/
public static HungrySingleton getInstace() {
return instance;
}

}

3.2 懒汉式

  • 用到时才初始化单例对象
  • 获取单例对象的方法加有锁 synchronized,获取单例时需要加锁、解锁,性能低并且并发度也低
  • 优点是支持延迟加载
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
/**
* 懒汉单例模式
*/
public class LazySingleton {

// 保证在所有线程中保持同步
private static volatile LazySingleton instance = null;

// 避免在外部被实例化
private LazySingleton(){}

/**
* 同步获取单例
* @return
*/
public static synchronized LazySingleton getInstance() {
// 需要在判断之前同步
if (instance == null) {
instance = new LazySingleton();
}

return instance;
}

}

3.3 双重检测

  • 用到时才初始化单例对象
  • 加锁 synchronized 创建单例,同时单例对象还需要加上关键字 volatile,避免指令重排和同步内存对象
  • 优点是支持延迟加载,除了第一次需要加锁以外,其他情况下都不需要加锁,所以性能也比价高,并发度也高
  • 缺点是,volatile 修饰的变量是到主存读取数据,不走缓存,这个稍微消耗点性能
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
/**
* 双重校验锁单例模式
*/
public class DoubleCheckLockSingleton {

// 保证在所有线程中保持同步
private static volatile DoubleCheckLockSingleton instance = null;

// 避免在外部被实例化
private DoubleCheckLockSingleton(){}

/**
* 检查两次,一次不加锁,一次加锁
* @return 单例对象
*/
public static DoubleCheckLockSingleton getInstance(){
// 第一次检查,不加锁
if (instance == null) {
synchronized (DoubleCheckLockSingleton.class) {
// 第二次检查,加锁
if (instance == null) {
instance = new DoubleCheckLockSingleton();
}
}
}
return instance;
}

}

3.4 静态内部类

  • 用到时才加载单例对象
  • 是在静态内部类加载时初始化好单例对象的
  • 不存在线程安全问题
  • 优点是支持延迟加载,不需要加锁,性能高,并发度高。总体上比双重检测上要好
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
/**
* 静态内部类单例模式
*/
public class StaticInnerSingleton {

// 避免在外部被实例化
private StaticInnerSingleton(){}

/**
* 类级的内部类,也就是静态的成员式内部类,
* 该内部类的实例与外部类的实例没有绑定关系,
* 而且只有被调用到才会装载,从而实现了延迟加载
*/
private static class SingletonHolder {
private static StaticInnerSingleton instance = new StaticInnerSingleton();
}

/**
* 直接获取单例
* @return 单例对象
*/
public static StaticInnerSingleton getInstance() {
return SingletonHolder.instance;
}

}

3.5 枚举类

  • 类加载时就初始化好单例了
  • 不支持延迟加载
  • 优点是创建单例对象简单,只需定义枚举对象即可
1
2
3
4
5
6
/**
* 枚举模式单例
*/
public enum EnumSingleton {
INSTANCE;
}

四、单例存在哪些问题?

4.1 单例对 OOP 特性的支持不友好

  • 单例对于抽象、继承、多态这几个特性的支持不太好
  • 单例对象的使用,是直接调用的,没有用依赖注入、基于接口调用的形式,因此在抽象方面支持的不是很好
  • 单例类,一般也不会继承,所以继承和多态,基本上是用不到的

4.2 单例会隐藏类之间的依赖关系

  • 一般通过构造函数、参数传递等方式声明的类之间的依赖关系,就能明确知道类的依赖关系
  • 单例的调用,不是通过依赖注入、参数传递来调用的
  • 单例对象一般都是在代码中直接调用,想要知道类的依赖关系,还需要看代码实现,不够明显

4.3 单例对代码的扩展性不友好

  • 单例类,只有一个单例对象,一般也不会继承
  • 单例类,想要添加功能,只能修改单例类的代码,对于可扩展性来说不太友好

4.4 单例对代码的可测试性不友好

  • 单例类这种硬编码式的使用方式(在代码里直接调用),无法实现 mock 替换,可测试性不强
  • 单例对象相当于一个全局对象,在单元测试时,还需要注意各个测试用例之间有没有影响到单例对象的数据,必须测试时受到影响。

4.5 单例不支持有参数的构造函数

  • 单例一般是无参的
  • 想要支持参数,就需要考虑每次传不同参数,以及参数什么时候传进去等情况,比较麻烦
  • 最好的办法是,单例初始化时,自己去读取配置文件来初始化,无需外部传参

五、单例有何替代解决方案?

  • 使用静态方法,缺点是静态方法不够灵活,也不支持延迟加载

  • 可能要从根上,寻找其他方式来实现全局唯一类了。比如,通过工厂模式、IOC 容器(比如 Spring IOC 容器)来保证,由程序员自己来保证(自己在编写代码的时候自己保证不要创建两个类对象)

  • 如果单例类并没有后续扩展的需求,并且不依赖外部系统,那设计成单例类就没有太大问题

作者

jiaduo

发布于

2022-01-15

更新于

2023-04-03

许可协议