该并发学习系列以阅读《Java并发编程的艺术》一书的笔记为蓝本,汇集一些阅读过程中找到的解惑资料而成。这是一个边看边写的系列,有兴趣的也可以先自行购买此书学习。 本文首发:windCoder.com
关于双重检测锁定,了解过单例的应该不陌生,但也容易写错。这里以单例模式为例一起探索。
首先看一下基本的懒汉模式
public class Singleton {
private static Singleton instance;
private Singleton(){}
public static Singleton getInstance() {
if(instance==null) { // 1:A线程执行
instance = new Singleton(); // 2:B线程执行
}
return instance;
}
}
我们都知晓这种方式不是线程安全的,在多线程吓不能正常工作:当A线程执行代码1的同时,B线程执行代码2.此时,A线程可能会看到instance引用的对象还未初始化。
对此,我们可以对getInstance()方法做同步处理来实现线程安全的延迟初始化,其优化如下:
public class Singleton {
private static Singleton instance;
private Singleton(){}
public synchronized static Singleton getInstance() {
if(instance==null) {
instance = new Singleton();
}
return instance;
}
}
由于对getInstance()方法做了同步处理,synchronize将导致性能开销。如果getInstance()方法被多个线程频繁的调用,将会导致程序执行性能的下降。
在早期JMM中,synchronized(甚至是无竞争的synchronized)存在巨大性能问题。为了继续优化,因此人们想出了一个“聪明”的技巧,即双重检查锁定(Double-Checked Locking,简称DCL):
public class Singleton { // 1
private static Singleton instance; // 2
private Singleton(){}
public static Signleton getInstance() { // 3
if(instance == null) { // 4:第一次检查
synchronized(Singleton.class) { // 5:加锁
if(instance == null) { // 6:第二次检查
instrance = new Singleton(); // 7:问题根源
}
} // 8
} // 9
return instance; // 10
} // 11
}
上面的看起来两全其美:
虽然看起来完美,但是一个错误的优化。在线程执行到第4行,代码读取到instance不为null时,instance引用的对象可能还没初始化完成。下面看一下具体根源。
实例化一个对象(如第7行的instrance=new Singleton();
)要分三个步骤:
但由于重排序,2和3可能发生重排序(在一些JIT编译器上,这种重排序是真实发生的),其过程如下:
所有线程在执行Java程序时必须要遵守intra-thread semantics。intra-thread semantics保证重排序不会改变单线程内的程序执行结果。
上面的2、3的重排序在没改变单线程程序的执行结果的前提下,可以提高程序的执行性能,故并未违反intra-thread semantics。然A线程正常执行时,B线程将看到一个还没被初始化的对象:B线程会导致第二个判断出错,instance != null,但它获得的仅是一个地址,此时A线程还未初始化,故B线程返回的instance对象是一个没有初始化的对象,如图:
知晓问题根源后,可以想到两个办法来解决:
对于上面基于DCL方案只需做一点小的修改即可,亦既把instance声明为volatile型:
public class Singleton {
private volatile static Singleton instance;
private Singleton(){}
public static Signleton getInstance() {
if(instance == null) {
synchronized(Singleton.class) {
if(instance == null) {
instrance = new Singleton();
}
}
}
return instance;
}
}
当instance声明为volatile后,步骤2、3的重排序在多线程环境中将会被禁止,从而解决问题。该解决方案需要JDK5及以上。
JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在此期间,JVM会获取一个锁。这个锁可以同步多个线程对同一个类的初始化。
基于该特性,可以实现另一种线程安全的延迟初始化方案,该方案被称之为Initialization On Demand Holder idiom:
public class Singleton {
private static class InstanceHolder {
public static Singleton instance = new Singleton();
}
private Singleton(){}
public static Signleton getInstance() {
return InstanceHolder.instance; // 这里将导致 InstanceHolder类被初始化
}
}
该方案的解决是指是:允许2和3重排序,但不允许非构造线程(此处指B线程)“看到”这个重排序。执行示意图如下:
初始化一个类,包括执行这个类的静态初始化和初始化在这个类中声明的静态自动。根据Java语言规范,在首次发生下列任意一种情况时,一个类或接口类型T将被立即初始化:
在Singleton中,首次执行getInstance()方法的线程将导致InstanceHolder类被初始化(符合情况4)。