系列文章目录
文章目录
- 系列文章目录
- 前言
前言
前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,这篇文章男女通用,看懂了就去分享给你的码吧。
双重检查锁(Double-Check Locking),顾名思义,通过两次检查,并基于加锁机制,实现某个功能。
在实现单例模式时,如果未考虑多线程的情况,就容易写出下面的getInstance1()错误代码:
public class Singleton {
private static volatile Singleton INSTANCE = null;
private Singleton() {
}
public static Singleton getInstance1() {
// 此处如果有多个执行流同时进入,会造成多次初始化
if (null == INSTANCE) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
public synchronized Singleton getInstance2() {
if (null == INSTANCE) {
INSTANCE = new Singleton();
}
return INSTANCE;
}
public static Singleton getInstance3() {
// 第1次,一般性检查,但是有并发隐患:可能有多执行流同时进入改处
if (null == INSTANCE) {
synchronized(Singleton.class) {
// 此处第2次检查,为了防止后续多执行流并发时,后续获取同步锁的执行流,不会再次初始化Singleton对象
if (null == INSTANCE) {
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
上述代码getInstance1()中,对单例对象INSTANCE进行判空检查,如果为null,则进行初始化。
这一步在单执行流的逻辑上是没有问题的。但是当多个执行流同时运行到此处时,如果执行流a正在初始化Singleton对象,还没返回其引用,就被调度出去了,此时执行流b也会进入此处,再次对Singleton对象进行初始化。如此一来,JVM中就会存在多个Singleton实例。
对于方法getInstance2()中,这样虽然解决了问题,但是因为用到了synchronized,会导致很大的性能开销,并且加锁其实只需要在第一次初始化的时候用到,之后的调用都没必要再进行加锁。
双重检查锁(double checked locking)是对上述问题的一种优化。先判断对象是否已经被初始化,再决定要不要加锁。
如上getInstance3()中,第1次检查,用来判断是否需要对Singleton进行初始化;如果是,则先加同步锁(此时可能有多个执行流都运行到改处);获得锁之后,第2次检查Singleton对象是否已被其他并发的执行流初始化了(这个null判空检查有隐患,后续阐明);如果两次检查都通过,则表明当前执行流,是第一个进入临界区的,因此可以担负对Singleton对象初始化的责任。由于同步加锁及第2次检查的存在,后续其他的执行流,即使同时进入临界区外等待,也不会出现对Singleton对象多次初始化的问题。
由于对象初始化的过程并不是原子的指令,无法在单个指令周期完成,又Java编译器对指令重排序优化的存在,对象初始化的操作流程会发生变化。
原始流程:
op1:分配内存空间
op2:初始化对象
op3:将对象的引用,指向分配的内存
指令重排序优化之后的流程:
op1:分配内存空间
op2:将对象的引用,指向分配的内存
op3:初始化对象
由于对象初始化流程的非原子性,当前执行流很可能在新流程的op2->op3这一步被调度出去,进而导致JVM中存在着一个已开辟内存空间、但是未初始化的Singleton实例。如果此时,其他调度进来的执行流使用了这个残缺的Singleton实例,很有可能因为数据异常引发运行时错误。
为此,我们需要一个机制,来阻止编译器对指令的重排序——这就是关键字 volatile。
加了 volatile 关键字的变量,编译器不会对其初始化指令进行重排序优化。因此就避免了上述的问题发生。
private static volatile Singleton INSTANCE = null;