双重检查锁的由来
在单例模式中,有一个DCL(双重锁)的实现方式,在Java程序中,很多时候需要推迟一些高开销的对象初始化操作,并且只有在使用这些对象的时候才进行开始初始化。
先来看下面实现单例的方式:
非线性安全的延迟初始化对象方式:
public class Test1 {
private static SingletonInstance instance;
private Test1(){}
public static SingletonInstance getInstance(){
if (null == instance){
instance = new SingletonInstance();
}
return instance;
}
}
上面的这种实现方式在高并发的环境下是有问题的,我们可以对getInstance方法做同步处理来实现线性安全,如下:
public class Test1 {
private static SingletonInstance instance;
private Test1() { }
public synchronized static SingletonInstance getInstance(){
if (null == instance){
instance = new SingletonInstance();
}
return instance;
}
}
但是这种同步方式会导致性能的开销,若getInstance被多个线程频繁调用,这将会导致程序执行性能的下降。只有在线程调用不多的场景下才可以,性能的开销可以忽略不计。
基于上述的问题,后来有人提出来了双重检查(Double-Checked Locking)的方法,通过双重检查来降低同步带来的性能损耗,如下:
public class DoubleCheckedLockingTest {
private static SingletonInstance instance;
private DoubleCheckedLockingTest() { }
public static SingletonInstance getInstance(){
if (null == instance){
synchronized (DoubleCheckedLockingTest.class) {
if (null == instance) {
instance = new SingletonInstance();
}
}
}
return instance;
}
}
乍一看,是很完美的解决了损耗问题,但是这种做法是错误的。
在line7 : instance = new SingletonInstance();创建单例对象的时候可以分解为下面三行伪代码:
//1、为对象分配内存空间
memory = allocation();
//2、初始化对象
initInstance(memory);
//3、设置instance指向刚刚分配的内存空间地址
instance = memory;
在JIT等编译的时候2-3可能会被重排,如重排后的结果如下:
//1、为对象分配内存空间
memory = allocation();
//3、设置instance指向刚刚分配的内存空间地址
instance = memory;
//2、初始化对象
initInstance(memory);
因此例如在line4的检查的时候instance可能还没有完全初始化好。这也导致了问题的根源所在。
为了解决重排的问题,我们就可以使用volatile关键字,来保证。
public class SalfDoubleCheckedLockingTest {
private volatile static SingletonInstance instance;
private SalfDoubleCheckedLockingTest () { }
public static SingletonInstance getInstance(){
if (null == instance){
synchronized (SalfDoubleCheckedLockingTest.class) {
if (null == instance) {
instance = new SingletonInstance();
}
}
}
return instance;
}
}
另外除了使用volatile关键字之外,还可以使用静态内部类的方式实现线程安全的单例,如下:
public class StaticClassInstance {
private StaticClassInstance(){}
private static class InstanceHandler{
private static SingletonInstance instance = new SingletonInstance();
}
public static SingletonInstance getInstance(){
return InstanceHandler.instance; //在此处会使InstanceHandler类被初始化
}
}
这种方式是基于JVM在类的初始化阶段(加载完成后并且未被线程使用之前),会执行类的初始化,在执行类的初始化期间,JVM会去获取一个锁,该锁可以同步多个线程对同一个类的初始化。
其实双重检查锁定(DCL)模式经常会出现在一些框架源码中,目的是为了延迟初始化变量。这个模式还可以用来创建单例。下面来看一个 Spring 中双重检查锁定的例子。
public class DefaultNamespaceHandlerResolver implements NamespaceHandlerResolver {
/** Stores the mappings from namespace URI to NamespaceHandler class name / instance. */
@Nullable
private volatile Map handlerMappings;
/**
* Load the specified NamespaceHandler mappings lazily.
*/
private Map getHandlerMappings() {
Map handlerMappings = this.handlerMappings;
if (handlerMappings == null) {
synchronized (this) {
handlerMappings = this.handlerMappings;
if (handlerMappings == null) {
if (logger.isTraceEnabled()) {
logger.trace("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]");
}
try {
Properties mappings =
PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
if (logger.isTraceEnabled()) {
logger.trace("Loaded NamespaceHandler mappings: " + mappings);
}
handlerMappings = new ConcurrentHashMap<>(mappings.size());
CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
this.handlerMappings = handlerMappings;
}
catch (IOException ex) {
throw new IllegalStateException(
"Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex);
}
}
}
}
return handlerMappings;
}
}