遅延初期化スレッドセキュリティ実装

13214 ワード

1.遅延初期化の不適切な実現
  • 非スレッドセキュリティバージョン
  • 単一のモードでは、遅延初期化(lazy initialization)が使用されることが多い.すなわち、最初の使用時にオブジェクトを初期化し、オーバーヘッドの高いオブジェクトの初期化作業を遅らせる.次は非スレッドセキュリティの遅延初期化コードです.
    public class Singleton {
        private static Singleton uniqueSingleton;
    
        private Singleton() {
        }
    
        public static Singleton getInstance() {
            if (null == uniqueSingleton) {
                uniqueSingleton = new Singleton();
            }
            return uniqueSingleton;
        }
    }
    

    このコードの問題は、マルチスレッドの場合、次のアクセス順序が発生する可能性があることです.
    Time
    Thread A
    Thread B
    T1
    uniqueSingletonが空であることを確認
    T2
    uniqueSingletonが空であることを確認
    T3
    イニシャルオブジェクトA
    T4
    オブジェクトAに戻る
    T5
    イニシャルオブジェクトB
    T6
    オブジェクトBに戻る
    この2つのスレッドは,このクラスの2つの異なるインスタンスを有し,単一例モードの初心に反している.
  • 同期ロックバージョン
  • synchronizedキーワードで方法を修飾し、同期ロックを加えることができます.
    public class Singleton {
        private static Singleton uniqueSingleton;
    
        private Singleton() {
        }
    
        public static synchronized Singleton getInstance() {
            if (null == uniqueSingleton) {
                uniqueSingleton = new Singleton();
            }
            return uniqueSingleton;
        }
    }
    

    しかし、synchronizedはパフォーマンスオーバーヘッドを引き起こし、実際には最初の初期化時にのみロックされ、後で呼び出される場合はロックする必要はありません.
  • エラーのデュアルチェックロックバージョン
  • では、最適化を検討して、二重チェックロックを使用することができます.ロックをかける前に一度チェックして、オブジェクトの初期化が完了したら、ロックを追加する必要はありません.コードは以下の通りです.
    public class Singleton {
        private static Singleton uniqueSingleton;
    
        private Singleton() {
        }
    
        public static Singleton getInstance() {
            if (null == uniqueSingleton) {
                synchronized (Singleton.class) {
                    if (null == uniqueSingleton) {
                        uniqueSingleton = new Singleton(); // error
                    }
                }
            }
            return uniqueSingleton;
        }
    }
    

    これにより、複数のオブジェクトとパフォーマンスのオーバーヘッドが発生する問題が完全に解決されたようです.
  • 複数のスレッドが1回目のチェックを通過すると、1つのスレッドだけが最初に同期ロックを取得し、2回目のチェックを通過してオブジェクトを作成することができ、その後、1回目のチェックを通過した後、同期ロックを取得するスレッドは、2回目のチェックが通過しないため、オブジェクトを作成する必要はありません.1回目のチェックに合格していないスレッドは、1回目のチェックが合格していないため、オブジェクトを作成する必要はありません.
  • は、すべてのスレッドを同期する必要はありません.スレッドがオブジェクトを作成した後、他のすべてのスレッドが任意にチェックでき、オブジェクトが初期化され、返されていることがわかります.

  • 実はそうではない.
    2.スレッドの安全な遅延初期化——volatileに基づく二重チェックロック実現
    上の二重チェックロックのコードは実際にはエラーであり、問題の根源は、初期化オブジェクトが原子操作ではなく、再ソートが発生する可能性があることです.初期化オブジェクトのコード、すなわち上のコードにerrorがマークされている行は、i.メモリ空間の割り当てii.初期化オブジェクトiiii.割り当てられたばかりのメモリ空間にオブジェクトを向ける3つのステップに分解することができ、いくつかのJITコンパイラでは、パフォーマンスの理由で第2ステップと第3ステップが並べ替えられる可能性がある.2つのスレッドの実行順序は次のとおりです.
    Time
    Thread A
    Thread B
    T1
    uniqueSingletonが空であることを確認
    T2
    ロックの取得
    T3
    uniqueSingletonが空であることを再確認
    T4
    uniqueSingletonにメモリ領域を割り当てる
    T5
    uniqueSingletonをメモリ領域に向ける
    T6
    uniqueSingletonが空でないことを確認
    T7
    uniqueSingleton(初期化されていないオブジェクト)へのアクセス
    T8
    uniqueSingletonの初期化
    このとき、スレッドBは、初期化されていないオブジェクト(ランダム値を有するメモリ)を読み出す.この問題を解決するためには,volatileキーワードで変数uniqueSingletonを修飾するだけでよいが,正しい二重チェックロックの実装コードは以下の通りである.
    public class Singleton {
        private volatile static Singleton uniqueSingleton;
    
        private Singleton() {
        }
    
        public static Singleton getInstance() {
            if (null == uniqueSingleton) {
                synchronized (Singleton.class) {
                    if (null == uniqueSingleton) {
                        uniqueSingleton = new Singleton();
                    }
                }
            }
            return uniqueSingleton;
        }
    }
    
    volatileを使用すると、並べ替えが禁止され、すべての書き込み操作(write)が読み取り操作(read)の前に発生します.
    3.スレッドの安全な遅延初期化——クラス初期化ロックに基づく実現
    JVMはクラスの初期化フェーズ(つまりクラスがロードされ、スレッドが使用される前)でクラスの初期化を実行します.クラスの初期化を実行する間、JVMはロックを取得します.このロックは、複数のスレッドによる同じクラスの初期化を同期できます.この特性に基づいて、コードは以下のように、別のスレッドセキュリティの遅延初期化スキーム(initialization-on-demand holder idiomと呼ばれる)を実現することができる.
    public class Singleton {
        private static class SingletonHolder {
            public static Singleton uniqueSingleton = new Singleton();
        }
    
        private Singleton() {
        }
    
        public static Singleton getInstance() {
            return SingletonHolder.uniqueSingleton;
        }
    }
    

    このスキームはJavaにおけるクラスの初期化ロックLCを利用している.JLS(Java SE 8 Edition)の「§12.4.1 When Initialization Occurs」セクションの説明に従います.
    A class or interface type T will be initialized immediately before the first occurrence of any one of the following:
  • T is a class and an instance of T is created.
  • A static method declared by T is invoked.
  • A static field declared by T is assigned.
  • A static field declared by T is used and the field is not a constant variable (§4.12.4).
  • T is a top level class (§7.6) and an assert statement (§14.10) lexically nested within T (§8.1.3) is executed.

  • 上記のコードではgetInstance()メソッドが初めて呼び出されると、上記の4つ目のケースに該当し、クラスSingletonHolderが初期化され、LCを用いて同期される.これにより,JVMによるクラス初期化ロックが利用され,遅延初期化されたスレッドセキュリティバージョンが実現される.LCの詳細については、参考文献[5]の「§12.4.2.Detailed Initialization Procedure」の項を参照してください.
    4.参考文献:
    [1]二重チェックロックと遅延初期化http://www.infoq.com/cn/articles/double-checked-locking-with-delay-initialization#anch136785 [2]Javaでのダブルチェックロック(double checked locking)https://www.cnblogs.com/xz816111/p/8470048.html [3] Double-checked locking and the Singleton pattern https://www.ibm.com/developerworks/java/library/j-dcl/index.html [4] Double-checked locking https://en.wikipedia.org/wiki/Double-checked_locking [5] The Java Language Specification, Java SE 8 Edition https://docs.oracle.com/javase/specs/jls/se8/html/jls-12.html#jls-12.4