雙重檢查鎖爲什麼要使用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;
	}



}

 

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