単一モードのステップアップの旅

5283 ワード

単一モード
Singleton(Singleton)は最も簡単で最も実用的な設計モデルの一つであり、「設計モード--オブジェクト向けソフトウェアを多重化できる基礎」という本では、単一のモデルを説明している.
  • 意図
  • クラスにインスタンスが1つしかないことを保証し、そのグローバル・アクセス・ポイントにアクセスします.
  • 動機
  • ...クラス自身に一意のインスタンスを保存させます.このクラスは、新しいオブジェクトの作成要求を切り取ることによって他のインスタンスが作成されないことを保証し、インスタンスにアクセスする方法を提供することができます.これがSingletonモードです.
    単純な単一モード
    Javaでは、最も簡単な単例モードは次のとおりです.
    public class Singleton1 {
    
        //           
        private static Singleton1 instance = new Singleton1();
    
        //        ,        
        private  Singleton1() {}
    
        //              
        public static Singleton1 getInstance() {
            return instance;
        }
    }
    

    だらだらロード
    ビジネスでは、この単一のインスタンスが呼び出されるとは限らず、最初からインスタンス化すると、スペースが無駄になる可能性があります.したがって、getInstance()メソッドを呼び出すときに、単一の例をインスタンス化する必要がある.
    public class Singleton2 {
    
        //        
        private static Singleton2 instance;
    
        private  Singleton2() {}
        
        public static Singleton2 getInstance() {
            //          
            if (instance == null){
                instance = new Singleton2();
            }
            return instance;
        }
    }
    

    同時セキュリティ
    前の単一例モードの実装では、同時シナリオにおいて、1つのスレッドがinstance==nullと判断し、まだinstanceがインスタンス化されていない間に、もう1つのスレッドもinstance==nullに達した場合、trueと判断され、結果として、2つのスレッドがそれぞれ1つのinstanceをインスタンス化し、これは私たちが単一例モードを使用する初心に反している.このような状況を避けるには、getInstance()メソッドにsynchronizedキーワードを追加し、同期メソッドであることを保証することも簡単です.
    public class Singleton3 {
    
        private static Singleton3 instance;
    
        private Singleton3() {}
    
        public synchronized static Singleton3 getInstance() {
            if (instance == null){
                instance = new Singleton3();
            }
            return instance;
        }
    }
    

    同時安全性の確保後の効率性の問題
    前の実装では、getInstance()メソッドを呼び出すとメソッド全体がロックされ、このメソッドに時間のかかるビジネスコードがいくつかある場合、プログラムの実行効率は比較的大きな影響を受けるため、synchronizedの役割の範囲を縮小する必要がある.
    public class Singleton4 {
    
        private static Singleton4 instance;
    
        private Singleton4() {}
    
        public static Singleton4 getInstance() {
    
            //     ...
    
            if (instance == null) {
                synchronized(Singleton4.class) {
                    if(instance == null) {
                        instance = new Singleton4();
                    }
                }
            }
            return instance;
        }
    }
    

    この実装では、方法をロックしない前提でいくつかのビジネスロジックを実行し、その後、2つのスレッドが同時にinstance==nullを判断し、1つのスレッドだけがSingleton.classのロックを取得し、instanceのオブジェクトをインスタンス化し、その後、別のスレッドもロックを取得した場合、2回目の判断instance==nullfalseであり、前のスレッドがインスタンス化されたinstanceの単一例が直接返されます.この方法はDCL(Double Check Lock,二重チェックロック)と呼ばれ,基本的に我々のニーズに達している.
    volatile
    では、このような実現は万全ではないでしょうか.わけではない.ここでは、より下位の知識について説明します.
    すべてのプログラミング言語は、最終的にCPUが実行するために命令に変換されることを知っています.例えば、JavaにオブジェクトObject o = new Object()を作成すると、少なくとも次の3つのCPU命令が含まれます.
  • メモリ内でオブジェクトに空間を開くと、そのオブジェクトの状態を「半初期化」と呼び、各メンバーの値がデフォルト値、例えばintタイプのデフォルト値が0、参照タイプのデフォルト値がnull
  • となる.
  • はObjectの構造方法を呼び出し、各メンバーは初期化され、例えば:int i = 1
  • oの参照をオブジェクト
  • に向ける.
    一方,CPUは実行効率のために,いくつかの命令を並べ替える.例えば、第2ステップでObjectを初期化する操作に時間がかかる場合があり、第3ステップに影響を及ぼさない場合、CPUはまず第3ステップを実行し、oを開いたメモリ領域に指向してからoを初期化する可能性があります.
    では、これは私たちの単例モードにどのような影響を及ぼしますか?
    さらに、2つのスレッドがgetInstance()を同時に呼び出すシーンをシミュレートします.スレッドAはSingleton4.classロックを取得した後、instanceを初期化する過程で、命令の再配置のため、instanceの参照をメモリ領域に指し示しますが、instanceオブジェクトの初期化が完了していません.instanceは半初期化された状態です.このときスレッドBは、instanceを2回目に判断したとき、nullではないことを発見し、このinstanceのオブジェクトに直接戻り、このinstanceの各メンバー変数はまだ付与されていない.
    実際の生産では、このような問題が発生する確率は極めて低いが、いったん発生すると、大きな損失をもたらし、調査が困難になる可能性がある.このような問題を回避するためには,CPUの「命令並び替え」操作を禁止することが重要である.Javaではvolatileのキーワードが提供されています.volatileの役割は2つあります.
  • メモリの可視性を保証する
  • 禁止命令並び替え
  • メモリの可視性の保証について簡単に説明します.
    メモリバリアが存在するため、スレッドが変数を操作すると、まずメインメモリからその変数のコピーを取得して自分のワークメモリに格納し、操作が完了してからメインメモリに書き込み、各スレッドのワークメモリ間は隔離されます.volatileメモリの可視性を保証するとは、スレッドが変数を操作するたびに、メインメモリからの再読み込みが強制され、メインメモリに保存が完了すると、他のスレッドにメインメモリから変数の再更新を通知することを意味します.即時更新のため、各スレッド操作の変数は、キャッシュのコピーではなく、同じと見なすことができ、変数に対する操作は互いに表示され、すなわち「メモリ可視性」である.instanceのインスタンスを宣言するときにvolatileのキーワードを加えると、上述した命令の並べ替えによる問題を回避することができる.
    public class Singleton5 {
    
        private static volatile Singleton5 instance;
    
        private Singleton5() {}
    
        public static Singleton5 getInstance() {
            if (instance == null) {
                synchronized(Singleton5.class) {
                    if(instance == null) {
                        instance = new Singleton5();
                    }
                }
            }
            return instance;
        }
    }
    

    これが最終版のDCLで、怠惰なロード、同時安全などの一連の要求を実現した.
    その他の方法
    上記の方法に加えて、Javaにおける単例モードは静的内部クラス、内部列挙クラスによって実現することができ、実際には、列挙を用いて単例モードを実現することは『Effective Java』という本の中で最も推奨されている方式であり、コードが簡潔であるだけでなく、DCL方式に比べて反射に基づく単例モードの破壊を防ぐことができる.
    public enum  EnumSingleton {
        INSTANCE;
        public EnumSingleton getInstance() {
            return INSTANCE;
        }
    }
    

    列挙方式は、最下位レベルで同時のセキュリティチェックを実現し、反射によってオブジェクトを作成すると、このクラスが列挙クラスであるため、異常が直接放出されます.
    単一要素の列挙タイプはSingletonを実現する最良の方法となっている.