マルチスレッドでの二重チェックロックについて


前言:前に単例モードを紹介するスレッドセキュリティのモードを見ましたが、後で同時書籍を見てみると、このスレッドセキュリティのモードを修正すると隠れた危険があることがわかりましたので、記録します.
参考書:『Java同時プログラミングの芸術』

まずこの例を見てみましょう


二重チェックロックは、uniqueInstanceが初期化されているかどうかを判断し、インスタンス化されていない場合はインスタンス化文をロックします.
    public class Singleton {
        private volatile static Singleton uniqueInstance;
        private Singleton() {
        }
        public static Singleton getUniqueInstance() {
            if (uniqueInstance == null) {
                synchronized (Singleton.class) {
                    if (uniqueInstance == null) {
                        uniqueInstance = new Singleton();
                    }
                }
            }
            return uniqueInstance;
        }
    }

次のバージョンに変更します(uniqueInstance前のvolatileキーワードが削除されました):
    public class Singleton {
        private static Singleton uniqueInstance;
        private Singleton() {
        }
        public static Singleton getUniqueInstance() {
            if (uniqueInstance == null) {
                synchronized (Singleton.class) {
                    if (uniqueInstance == null) {
                        uniqueInstance = new Singleton();
                    }
                }
            }
            return uniqueInstance;
        }
    }

そして質問を紹介します


まず、ロックの二重チェックの目的は、初期化を遅延させて初期化クラスとオブジェクトの作成のオーバーヘッドを低減することであり、スレッドの安全を保障しながらロックの使用を低減することである(nullのために同期ブロックを追加しないで直接返さない).しかし2つ目は成功しなかったようです...
私が変更したコードは最初から見て、正常で、スレッドのセキュリティの問題は発生しません:uniqueInstanceを初めてチェックしたら!=nullでは、次のロックおよび初期化操作を実行する必要はありません.従ってsynchronizedによるパフォーマンスオーバーヘッドを大幅に低減することができる.この理解は両立しているように見える.しかし、これは誤った最適化です!スレッドがif (uniqueInstance == null)まで実行されると、uniqueInstance参照のオブジェクトが初期化されていない可能性があります.

問題の根源


前の二重チェックロックサンプルコードの行uniqueInstance = new Singleton();は、オブジェクトを作成する.この行コードは、以下の3行の擬似コードに分解することができる.
memory = allocate(); // 1. 
ctorInstance(memory); // 2. 
uniqueInstance = memory; // 3. uniqueInstance 

上の3行の疑似コードの2と3の間には、並べ替えられる可能性があります.2と3の並べ替え後の実行タイミングは以下の通りです.
memory = allocate(); // 1. 
uniqueInstance = memory; // 3. uniqueInstance 
						//  : 
ctorInstance(memory); // 2. 

なぜここで再ソートが発生する可能性があるのかというと、2と3の再ソート後に単一スレッドプログラムの実行結果が変更されず、プログラムの実行性能が向上するからである.具体的な並べ替えの原因は余計な説明にすぎず、自分で調べる.
その後、uniqueInstance = new Singleton();で並べ替えが発生した後、1番目のスレッドはuniqueInstance = memoryに実行され、この時点では初期化されていないが、オブジェクトは生成され、2番目のスレッドはif (uniqueInstance == null)行でnullではないと判断し、return uniqueInstance;行に直接実行され、初期化されていないオブジェクトに戻る.

ソリューション


volatileベースのソリューション


uniqueInstanceの前にvolatileキーワードを直接追加すると、スレッドの安全な遅延初期化、すなわち私が変更していないコードを実現することができます.
public class Singleton {
        private volatile static Singleton uniqueInstance;
        private Singleton() {
        }
        public static Singleton getUniqueInstance() {
            if (uniqueInstance == null) {
                synchronized (Singleton.class) {
                    if (uniqueInstance == null) {
                        uniqueInstance = new Singleton();
                    }
                }
            }
            return uniqueInstance;
        }
    }

volatileと宣言すると、2と3の間の並べ替えはマルチスレッド環境では禁止され、スレッドの安全な遅延初期化が保証されます.
注意:volatileの意味の1つ目は可視性を保証することであり、2つ目は命令の再ソート最適化を禁止することである.volatileシールド命令の並べ替えの意味はJDK 5で完全に修復されたが,これはJDK 5バージョン以前のこの解決策が通用しなかったことを意味する.(このソリューションでは、JDK 5以降のバージョンが必要です.JDK 5から新しいJSR-133メモリモデル仕様が使用されるため、volatileの意味が強化されます.)

クラス初期化ベースのソリューション


JVMはクラスの初期化フェーズ(つまりクラスがロードされ、スレッドが使用される前)でクラスの初期化を実行します.クラスの初期化を実行する間、JVMはロックを取得します.このロックは、複数のスレッドによる同じクラスの初期化を同期できます.
この特性に基づいて,別のスレッドセキュリティの遅延初期化スキームを実現できる.
public class Singleton {

	private static class InstanceHolder {
		public static Singleton uniqueInstance = new Singleton();
	}

	private Singleton() {}

	public static Singleton getUniqueInstance() {
		return InstanceHolder.uniqueInstance; //  InstanceHolder 
	}
}


このスキームの本質は、前に述べた3と2の再ソートを許可するが、非構造スレッドがこの再ソートを「見る」ことを許可しないことである.すなわち、この初期化ロックを取得した最初のスレッドのみがこの再ソートを見ることができ、他の後に初期化ロックを取得したスレッドはこの再ソートプロセスを見ることができない.
クラス初期化に関する具体的な解釈は『Java同時プログラミングの芸術』の第3章Javaメモリモデルの3.8節を見ることができる