[設計モード]単例モードの各種書き方


(「javaで完璧な単例モードを書く」、「Effective Java」(第3版)第3条:Singleton属性P 24/306をプライベートコンストラクタまたは列挙タイプで強化する)、「なぜ最も良い単例モードは列挙単例であるのか」、「Java同時メモ-単例と二重検出」を参照)
1.1怠け者式(遅延ロード)
単純バージョン
public class Singleton{
    private static Singleton instance;
    private Singleton(){}
    public static Singleton getInstance(){
        if(instance==null){
            instance = new Singleton();
        }
        return instance;
    }
}

問題は、マルチスレッドが動作する場合、if(instance==null)まで複数のスレッドが同時に実行され、nullと判断されると、2つのスレッドがそれぞれ1つのインスタンスを作成することであり、これは単一の例ではないということです.
synchronizedバージョン
public class Singleton{
    private static Singleton instance;
    private Singleton(){}
    public static synchronized Singleton getInstance(){
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

この書き方にも問題があります.getInstanceメソッドにロックをかけると、複数のインスタンスの問題が発生する可能性がありますが、臨界領域に入るスレッド以外のすべてのスレッドを強制的に待機させ、実際にはプログラムの実行効率に悪影響を及ぼす可能性があります.
二重検査バージョン
public class Singleton {
    private static Singleton instance;
    private Singleton() {}
    public static Single3 getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

しかし,この二重検出機構はJDK 1.5以前には問題があった.なぜこのバージョンで発生する可能性のある問題があるのかを明らかにするには、まず、原子操作、命令再配置といういくつかの概念を明らかにする必要があります.
簡単に言えば、原子操作(atomic)は分割不可能な操作であり、コンピュータでは、スレッドスケジューリングによって中断されない操作を指す.
命令の並べ替えは簡単に言えば、コンピュータが実行効率を高めるために行ういくつかの最適化であり、最終結果に影響を与えない場合、いくつかの文の実行順序を調整する可能性があります.
問題は主にsingleton=new Singleton()という文で、これは原子操作ではありません.
  • singletonにメモリを割り当てる
  • Singletonのコンストラクタを呼び出してメンバー変数を初期化し、インスタンス
  • を形成する.
  • singletonオブジェクトを割り当てられたメモリ領域(singletonがnullではない)
  • に指し示します.
    ただし、JVMのインスタント・コンパイラでは、コマンドの再ソートの最適化が存在します.すなわち、上記の2ステップ目と3ステップ目の順序は保証されず、最終的な実行順序は1−2−3であっても1−3−2であってもよい.後者であれば、3実行完了、2未実行の前にスレッド2によってプリエンプトされ、このときinstanceは既にnullではない(ただし初期化されていない)ので、スレッド2は直接instanceに戻り、使用し、そのままエラーを報告します.
    ここで重要なのは、スレッドT 1のinstanceに対する書き込みが完了していないことであり、スレッドT 2は読み取り操作を実行することである.
    究極のバージョン
    public class Singleton {
        private static volatile Singleton instance;
        private Singleton() {}
        public static Singleton getInstance() {
            if (instance == null) {
                synchronized (Singleton.class) {
                    if (instance == null) {
                        instance = new Singleton();
                    }
                }
            }
            return instance;
        }
    }
    

    JDK 1.5以降、volatileキーワードを使用して変数を修飾し、無秩序な書き込みによる問題を解決することができる.volatileキーワードの重要な役割の一つは、メモリ割り当て、オブジェクト参照の戻し、初期化という順序が発生しないことを保証し、二重検出が真に機能することである.instanceをvolatileと宣言すると、その書き込み操作にメモリバリア(メモリバリアとは何ですか?)があり、その付与が完了する前に読み取り操作を呼び出す必要はありません.
    注意:volatileはsingleton=new Singleton()という文の内部[1-2-3]の命令を並べ替えるのではなく、1つの書き込み操作([1-2-3])が完了するまで読み取り操作(if(instance==null))を呼び出さないことを保証します.
    しかし、どうしてもとげを選ばなければならないなら、この書き方は少し複雑で、優雅で簡潔ではありません.
    1.2餓漢式(非遅延負荷)
    上述したように、餓漢式単例とは、グローバルな単例インスタンスがクラスロード時に構築される実装形態を指す.
    クラスローディングのプロセスはクラスローダ(ClassLoader)によって実行されるため、このプロセスもJVMによって同期を保証するため、この方法には先天的に多くのマルチスレッドによる問題を免疫できるという利点がある.
    パブリックドメインメソッド
    public class Singleton{
        public static final Singleton INSTANCE = new Singleton();
        private Singleton(){}
    }
    

    特権を持つクライアントは、AccessibleObject.setAccessableメソッドを使用して、反射メカニズムを使用してプライベートコンストラクタを呼び出すことができます.この攻撃を防ぐには、コンストラクタを変更して、2番目のインスタンスの作成を要求されたときに例外を投げ出すことができます.
    公有ドメイン法の主な利点は,APIがこのクラスがSingletonであることを明確に示していることである:公有の静的ドメインはfinalであるため,このドメインは常に同じオブジェクト参照を含む.2つ目の利点は、それが簡単であることです.
    スタティツクファクトリメソッド
    public class Singleton{
        private static final Singleton INSTANCE = new Singleton();
        private Singleton(){}
        public static Singleton getInstance(){
            return INSTANCE;
        }
    }
    

    静的ファクトリアプローチの利点の1つは,そのAPIを変更することなく,このクラスがSingletonであるべきかどうかを変えることができる柔軟性を提供することである.ファクトリメソッドはクラスの一意のインスタンスを返しますが、メソッドを呼び出すスレッドごとに一意のインスタンスを返すなど、変更されやすいです.2つ目の利点は、アプリケーションが必要であれば、汎用Singletonファクトリを作成できることです.静的ファクトリを使用する最後の利点は、Singleton::instanceがSupplierであるなどのプロバイダとして参照できることです.以上のいずれかの利点を満たさない限り、公有ドメインの方法を優先的に考慮する.
    それらの欠点はただ餓漢式の単例自身の欠点である--INSTANCEの初期化はクラスのロード時に行われ、クラスのロードはClassLoaderによって行われたため、開発者はもともとその初期化のタイミングを正確に把握することが難しい.
  • 初期化が早すぎるため、リソースの浪費が発生する可能性があります.
  • 初期化自体が他のデータに依存している場合、他のデータが初期化される前に準備されることを保証することは難しい.

  • もちろん、必要な単一の例が占有するリソースが少なく、他のデータにも依存しない場合、このような実装も良好である.
    1.3 Effective Javaの書き方
    静的内部クラス
    『Effective Java』の第1版では、書き方をお勧めします.
    public class Singleton {
        private static class SingletonHolder {
            private static final Singleton INSTANCE = new Singleton();
        }
        private Singleton (){}
        public static final Singleton getInstance() {
            return SingletonHolder.INSTANCE;
        }
    }
    

    この書き方は非常に巧みです.
  • 内部類SingletonHolderについては、餓漢式の単例実装であり、SingletonHolderが初期化されるとClassLoaderによって同期が保証され、INSTANCEが真・単例となる.
  • また、SingletonHolderは内部クラスであり、外部クラスのSingletonのgetInstance()でのみ使用されるため、ロードされるタイミングはgetInstance()メソッドが初めて呼び出されたときである.

  • --ClassLoaderを利用して同期を保証し、開発者にクラスのロードのタイミングを制御させることができます.内部から見れば餓漢式の単例だが、外部から見れば怠け者式の実現である.
    まるで神妙な技だ.
    列挙
    Singletonを実装すると、単一の要素を含む列挙タイプを宣言することもできます.
    // Effective Java        
    public enum SingleInstance {
        INSTANCE;
        public void fun1() { 
            // do something
        }
    }
    //   
    SingleInstance.INSTANCE.fun1();
    

    列挙インスタンスを作成するプロセスはスレッドが安全であるため、この書き方も同期の問題はありません.
    列挙クラスはまた、反射方式で列挙オブジェクトを取得することを防止し、反射newInstanceメソッドを呼び出すと列挙クラスであるかどうかをチェックし、エラーが報告される場合は、Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects列挙クラスは、シーケンス化と逆シーケンス化を使用して新しい列挙オブジェクトを取得することを防止します.シーケンス化時にJavaは列挙オブジェクトのname属性のみを結果に出力し,逆シーケンス化時にはjava.lang.EnumのvalueOfメソッドで名前に基づいて列挙オブジェクトを検索する.同時に、コンパイラは、このようなシーケンス化メカニズムのカスタマイズを許可しないため、writeObject、readObject、readObject、readObject NoData、writeReplace、readResolveなどの方法を無効にします.
    Effective Java(第3版)での評価:この方法は公有ドメイン法と機能的に似ているが,より簡潔で無償でシーケンス化メカニズムを提供し,複雑なシーケンス化や反射攻撃に直面した場合でも,複数回のインスタンス化を絶対に防止する.この方法はまだ広く用いられていないが,単一要素の列挙タイプはSingletonを実現するための最適な方法とよく呼ばれる.SingletonがEnumを拡張するのではなくスーパークラスを拡張する必要がある場合、この方法は使用しないでください(インタフェースを実装するために列挙を宣言することができますが).