「这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战」
前言
作为开发者,单例这个就再也熟悉不过了,但是作为多种单例实现模式,我个人觉得双重校验锁是非常不多的实现,我们简单来分析一下其原理。
正文
先来说一下Java版本的,后面会涉及Kotlin中的代码我们再做比对。
代码实现
Java代码实现如下:
//双重校验锁单例
public class SingleInstance {
//必须volatile修饰 见分析1
private volatile static SingleInstance instance;
//私有化构造函数
private SingleInstance() {
}
public static SingleInstance getInstance() {
//第一个判空 见分析2
if (instance == null) {
synchronized (SingleInstance.class) {
//第二个判空 见分析3
if (instance == null) {
//新建实例
instance = new SingleInstance();
}
}
}
return instance;
}
}
首先这里synchronized
关键字没有修饰整个getInstance
函数,因为这个函数可能使用地方很多很频繁,如果修饰整个函数,这样就会造成其他线程阻塞,所以这里只同步了一段代码。
分析2:为什么在进入同步代码块时需要进行进行判空?假如有线程A和线程B,这时线程A先判断instance
为null
,所以它进入了同步代码块,创建了对象;然后线程B再进来时,它就不必再进入同步代码快了,可以直接返回,其实也就是懒加载,可以加快执行速度。
分析3:为什么在同步代码块中还要再进行一次判断呢?假如有线程A和线程B,它俩A先调用方法,B紧接着调用,这时A、B在分析2处的判空都是空,所以线程A进入同步代码块里面执行,线程B进行等待;当A进入同步代码块中创建了对象后,A线程释放了锁,这时B再进入,如果这时不加分析3的判空,B又会创建一个实例,这明显不符合规矩。
所以这里是针对多个线程都越过了第一次判空的情况。
分析1:那既然加了2层判断,那为什么还要加个volatile
关键字呢,这里知识点就有点多了。
因为新建实例的代码:
instance = new SingleInstance();
它不是一个原子操作,这个简单的赋值可以分为3步:
1、给SingleInstance
分配内存。
2、调用SingleInstance
的构造方法。
3、把instance
指向分配的内存空间,这时instance
就不是null
了。
这是正常逻辑的3个步骤,也只有按1->2->3
执行后,这个instance
才不是null
。
但是Java内存模型允许这个进行指令重排序,也就是这3步可能是1->2->3
也可能是1->3->2
,所以这里就有问题了。
假如线程A和线程B,线程A已经跑到分析3处的代码,这时这条指令执行是1->3->2
,刚把步骤3执行完,这时线程B跑到了分析2处的代码,会发现instance
不为null
了,这时线程B就直接返回了,从而导致错误。
既然知道了原因,那volatile
关键字就是解决这个的,它可以禁止指令重新排序,而且保证所有线程看到这个变量是一致的,也就是不会从缓存中读取(这个特性后面有机会再说),所以在创建instance
实例时,它的步骤都是1->2->3
,就不会出错了。
总结
其实一个小小的双重校验锁单例都还有蛮多知识点,后面我们来分析分析Kotlin中的单例都一般如何实现以及原理。