二重チェックロックvolatileフィールドを使用する理由

4742 ワード

二重検査錠の由来
一例モードでは、Javaプログラムでは、オーバーヘッドの高いオブジェクトの初期化操作を遅らせる必要があり、これらのオブジェクトを使用する場合にのみ初期化を開始するDCL(二重ロック)の実装がある.
まず、単一の例を実装する方法を見てみましょう.
非線形安全な遅延初期化オブジェクト方式:
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;
    }
}

一見、損失問題を完璧に解決したが、このやり方は間違っている.
line 7:instance=new SingletonInstance()単一のオブジェクトを作成するときは、次の3行の擬似コードに分解できます.
//1、         
memory = allocation(); 
//2、     
initInstance(memory);
//3、  instance             
instance = memory;

JITなどでコンパイルする場合は2-3が並べ替えられる可能性がありますが、並べ替え後の結果は以下の通りです.
//1、         
memory = allocation(); 
//3、  instance             
instance = memory;
//2、     
initInstance(memory);

したがって、例えばline 4のチェック時に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;
	}



}