单例模式的进阶之旅
Posted 2511zzz
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单例模式的进阶之旅相关的知识,希望对你有一定的参考价值。
单例模式
单例模式(Singleton)是最简单又最实用的设计模式之一,《设计模式——可复用面向对象软件的基础》一书中这样描述单例模式:
- 意图
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
- 动机
...让类自身负责保存它的唯一实例。这个类可以保证没有其他实例可以被创建(通过截取创建新对象的请求),并且它可以提供一个访问该实例的方法。这就是Singleton模式。
简单的单例模式
在Java中,一个最简单的单例模式是这样的:
public class Singleton1 {
// 创建这个类的唯一实例
private static Singleton1 instance = new Singleton1();
// 构造方法私有化,禁止外部创建实例
private Singleton1() {}
// 提供一个访问点用于获取单例
public static Singleton1 getInstance() {
return instance;
}
}
懒加载
业务中可能会有这样的需求:这个单例不一定会被调用,如果一开始就将其实例化的话,会有浪费空间的可能。因此,我们需要在调用到getInstance()
方法时再实例化单例。
public class Singleton2 {
// 只声明不初始化
private static Singleton2 instance;
private Singleton2() {}
public static Singleton2 getInstance() {
// 判断是否已被初始化
if (instance == null){
instance = new Singleton2();
}
return instance;
}
}
并发安全
对于上一种单例模式的实现,在并发情景下,如果在一个线程判断了instance==null
,而尚未实例化instance
之际,另一个线程也走到了instance==null
这一步,那么仍然会判断为true
,导致的后果就是两个线程分别实例化了一个instance,这违背了我们使用单例模式的初衷。想要避免这种情况,也很简单,就是给getInstance()
方法加上synchronized
关键字,保证这是一个同步方法。
public class Singleton3 {
private static Singleton3 instance;
private Singleton3() {}
public synchronized static Singleton3 getInstance() {
if (instance == null){
instance = new Singleton3();
}
return instance;
}
}
保证并发安全后的效率问题
上一种实现中,调用getInstance()
方法时会导致整个方法被锁住,如果这个方法中还有一些比较耗时的业务代码的话,程序运行的效率都会受到比较大的影响,因此,我们需要缩小synchronized
作用的范围。
public class Singleton4 {
private static Singleton4 instance;
private Singleton4() {}
public static Singleton4 getInstance() {
// 业务逻辑...
if (instance == null) {
synchronized(Singleton4.class) {
if(instance == null) {
instance = new Singleton4();
}
}
}
return instance;
}
}
在这种实现中,我们在不锁方法的前提下先执行一些业务逻辑,然后,如果此时有两个线程同时判断了instance==null
,只有一个线程能获取Singleton.class
的锁,然后实例化instance
对象,再然后另一个线程也获取到了锁,此时它第二次判断instance==null
为false
,就会直接返回上一个线程已经实例化的instance
单例。这种方法被称为DCL(Double Check Lock,双重校验锁),基本达到了我们的需求。
volatile
那么,刚刚这种实现是不是就万无一失了呢?并不是。这里涉及到了一些更底层的知识:
我们知道,所有编程语言最终都会转换成指令供CPU执行,例如在Java中创建一个对象Object o = new Object()
,就至少包含下面3条CPU指令:
- 在内存中为该对象开辟一块空间,此时,该对象的状态称为“半初始化”,各成员的值都是默认值,例如int类型的默认值为0,引用类型的默认值为null
- 调用Object的构造方法,各成员初始化,如:
int i = 1
- 将o的引用指向该对象
而CPU为了运行效率,会对一些指令进行重排。例如第2步中Object初始化的操作可能会比较耗时,而它又对第3步没有影响,CPU就可能会先执行第3步,将o指向开辟好的内存区域,然后再初始化o。
那么,这对我们的单例模式有什么影响呢?
我们再来模拟一下两个线程同时调用getInstance()
的场景:线程A获取Singleton4.class
锁之后,初始化instance过程中,由于指令重排,先将instance
的引用指向某块内存区域,然而尚未完成instance
对象的初始化,instance
处于一个半初始化的状态。此时线程B第二次判断instance
时发现它不为null
,就会直接返回这个instance
对象,而这个instance
中各个成员变量都尚未被赋值。
在实际生产中,这样的问题发生的机率极低,但是一旦发生,就可能造成很大的损失并且难以排查。想要避免这种问题,关键就在于禁止CPU的“指令重排序”操作。而Java中提供了volatile
关键字用于实现这一点,volatile
的作用有两点:
- 保证内存可见性
- 禁止指令重排序
简单介绍一下“保证内存可见性”:
由于内存屏障的存在,线程操作某一个变量时,会先从主内存中获取一个该变量的副本存入自己的工作内存中,操作完后再写入主内存,而各个线程的工作内存之间是隔离的。
volatile
保证内存可见性的意思就是每当线程操作一个变量时,都会强制重新从主内存中读取,操作完存入主内存时,也会通知其他线程重新从主内存更新该变量。由于即时更新的原因,各个线程操作的变量可以看作不是缓存的副本,而是同一个,对变量的操作是彼此可见的,也就是“内存可见性”。
在声明instance
实例时加上volatile
关键字,就可以避免上述的因指令重排所引发的问题。
public class Singleton5 {
private static volatile Singleton5 instance;
private Singleton5() {}
public static Singleton5 getInstance() {
if (instance == null) {
synchronized(Singleton5.class) {
if(instance == null) {
instance = new Singleton5();
}
}
}
return instance;
}
}
这就是最终版的DCL,实现了懒加载、并发安全等一系列要求。
其他方式
除了上述方法外,Java中的单例模式还能通过静态内部类、内部枚举类来实现,事实上,使用枚举实现单例模式是《Effective Java》一书中最为推荐的方式,它不仅代码简洁,并且与DCL方式相比,它还能抵御基于反射的对单例模式的破坏。
public enum EnumSingleton {
INSTANCE;
public EnumSingleton getInstance() {
return INSTANCE;
}
}
枚举方式在底层已经为我们实现了并发情况下的安全检查,并且通过反射创建对象时,由于该类是枚举类,会直接抛出异常。
单元素的枚举类型已经成为实现Singleton的最佳方法
以上是关于单例模式的进阶之旅的主要内容,如果未能解决你的问题,请参考以下文章
我的C语言学习进阶之旅解决 Visual Studio 2019 报错:错误 C4996 ‘fscanf‘: This function or variable may be unsafe.(代码片段
我的C语言学习进阶之旅解决 Visual Studio 2019 报错:错误 C4996 ‘fscanf‘: This function or variable may be unsafe.(代码片段
我的Android进阶之旅关于Android平台获取文件的mime类型:为啥不传小写后缀名就获取不到mimeType?为啥android 4.4系统获取不到webp格式的mimeType呢?(代码片段
我的Android进阶之旅关于Android平台获取文件的mime类型:为啥不传小写后缀名就获取不到mimeType?为啥android 4.4系统获取不到webp格式的mimeType呢?(代码片段