单例模式
单例模式
一、什么是单例模式?
单例设计模式(Singleton Design Pattern):指一个类只有一个实例,且该类能自行创建这个实例的一种模式。
这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。
单例模式有 3 个特点:
- 单例类只有一个实例对象
- 该单例对象必须由单例类自行创建
- 单例类对外提供一个访问该单例的全局访问点
二、为什么要使用单例?
从业务概念上,有些数据在系统中只应该保存一份,就比较适合设计为单例类。
- 表示全局唯一类,比如系统配置类
- 处理资源访问冲突,比如打印日志类
三、如何实现一个单例?
单例需要考虑以下几点:
- 单例的构造函数必须是
private
访问权限的,避免外部创建对象 - 考虑线程并发创建单例对象的情况,多线程同时创建单例对象时,是否能够保证只有一个单例生成
- 考虑是否要延迟加载的情况,比如单例对象是否要等到第一次获取的时候才生成,还是一开始就存在
- 考虑获取单例对象的性能问题,比如对方法加锁会导致性能变差
单例创建的几种方式:
3.1 饿汉式
- 在类加载期间,就已经把单例对象初始化好了
- 不存在线程安全问题
- 缺点是不支持延迟加载,没有用到该单例就已经初始化好对象了
1 | /** |
3.2 懒汉式
- 用到时才初始化单例对象
- 获取单例对象的方法加有锁
synchronized
,获取单例时需要加锁、解锁,性能低并且并发度也低 - 优点是支持延迟加载
1 | /** |
3.3 双重检测
- 用到时才初始化单例对象
- 加锁
synchronized
创建单例,同时单例对象还需要加上关键字volatile
,避免指令重排和同步内存对象 - 优点是支持延迟加载,除了第一次需要加锁以外,其他情况下都不需要加锁,所以性能也比价高,并发度也高
- 缺点是,
volatile
修饰的变量是到主存读取数据,不走缓存,这个稍微消耗点性能
1 | /** |
3.4 静态内部类
- 用到时才加载单例对象
- 是在静态内部类加载时初始化好单例对象的
- 不存在线程安全问题
- 优点是支持延迟加载,不需要加锁,性能高,并发度高。总体上比双重检测上要好
1 | /** |
3.5 枚举类
- 类加载时就初始化好单例了
- 不支持延迟加载
- 优点是创建单例对象简单,只需定义枚举对象即可
1 | /** |
四、单例存在哪些问题?
4.1 单例对 OOP 特性的支持不友好
- 单例对于抽象、继承、多态这几个特性的支持不太好
- 单例对象的使用,是直接调用的,没有用依赖注入、基于接口调用的形式,因此在抽象方面支持的不是很好
- 单例类,一般也不会继承,所以继承和多态,基本上是用不到的
4.2 单例会隐藏类之间的依赖关系
- 一般通过构造函数、参数传递等方式声明的类之间的依赖关系,就能明确知道类的依赖关系
- 单例的调用,不是通过依赖注入、参数传递来调用的
- 单例对象一般都是在代码中直接调用,想要知道类的依赖关系,还需要看代码实现,不够明显
4.3 单例对代码的扩展性不友好
- 单例类,只有一个单例对象,一般也不会继承
- 单例类,想要添加功能,只能修改单例类的代码,对于可扩展性来说不太友好
4.4 单例对代码的可测试性不友好
- 单例类这种硬编码式的使用方式(在代码里直接调用),无法实现 mock 替换,可测试性不强
- 单例对象相当于一个全局对象,在单元测试时,还需要注意各个测试用例之间有没有影响到单例对象的数据,必须测试时受到影响。
4.5 单例不支持有参数的构造函数
- 单例一般是无参的
- 想要支持参数,就需要考虑每次传不同参数,以及参数什么时候传进去等情况,比较麻烦
- 最好的办法是,单例初始化时,自己去读取配置文件来初始化,无需外部传参
五、单例有何替代解决方案?
使用静态方法,缺点是静态方法不够灵活,也不支持延迟加载
可能要从根上,寻找其他方式来实现全局唯一类了。比如,通过工厂模式、IOC 容器(比如 Spring IOC 容器)来保证,由程序员自己来保证(自己在编写代码的时候自己保证不要创建两个类对象)
如果单例类并没有后续扩展的需求,并且不依赖外部系统,那设计成单例类就没有太大问题