双重检查锁为什么要使用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;
    }
}

       但是这种同步方式会导致性能的开销,若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<String, Object> handlerMappings;

	/**
	 * Load the specified NamespaceHandler mappings lazily.
	 */
	private Map<String, Object> getHandlerMappings() {
		Map<String, Object> 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;
	}



}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章