双重检查锁为什么要使用volatile字段

双重检查锁的由来
在单例模式中,有一个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; } }

【双重检查锁为什么要使用volatile字段】但是这种同步方式会导致性能的开销,若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; }}


    推荐阅读