JAva単例設計モードで知らないかもしれない秘密


単純で効率的な方法では、単一のインスタンスモードを実現することができますが、任意の場合に単一のインスタンスの完全性を確保する方法はありません.
単一のモードとは、クラスが一度だけインスタンス化され、グローバルまたはシステム範囲を表すコンポーネントです.シングル・インスタンス・モードは、ログ・レコード、ファクトリ、ウィンドウ・マネージャ、プラットフォーム・コンポーネント管理などによく使用されます.実装されると変更や再ロードが困難になり、テスト用例の作成が困難になり、コード構造が悪いなどの問題が発生するため、単例モードの使用はできるだけ避けるべきだと思います.また,以下の文書の一例パターンは安全ではない.
単例モデルをどのようによりよく実現するかを研究するのに多くの精力が費やされているが,簡単で効率的な実現方法がある.しかしながら、いかなる場合においても、単一の例の完全性を確保する方法はない.次の文を読んで、あなたが認めているかどうかを見てみましょう.
Finalフィールド
この方法は構造関数を私有化し、公有のstatic finalオブジェクトを外部に提供する.
public class FooSingleton {
    public final static FooSingleton INSTANCE = new FooSingleton();
    private FooSingleton() { }
    public void bar() { }
}

クラスがロードされるとstaticオブジェクトが初期化され、プライベートなコンストラクション関数が最初で最後に呼び出されます.クラスの初期化前に複数のスレッドが呼び出されていても、JVMはスレッドの実行を継続するときにクラスが完全に初期化されていることを保証します.しかし、反射およびsetAccessible(true)メソッドを使用すると、他の新しいインスタンスを作成できます.
Constructor[] constructors = FooSingleton.class.getDeclaredConstructors();
Constructor constructor = constructors[0];
constructor.setAccessible(true);
FooSingleton spuriousFoo = (FooSingleton) constructor.newInstance(new Object[0]);

コンストラクション関数を変更して、複数回の呼び出しを回避する必要があります.たとえば、再呼び出し時に例外を放出します.次のようにFooSingletonコンストラクション関数を修正すると、このような攻撃を防ぐことができます.
public class FooSingleton2 {
    private static boolean INSTANCE_CREATED;
    public final static FooSingleton2 INSTANCE = new FooSingleton2();
    private FooSingleton2() {
        if (INSTANCE_CREATED) {
            throw new IllegalStateException("You must only create one instance of this class");
        } else {
            INSTANCE_CREATED = true;
        }
    }
    public void bar() { }
}

これで安全に見えますが、新しいインスタンスを作成するのは簡単です.INSTANCEを変更するだけですCREATEDフィールド、同じトリックをプレイすればいいです.
Field f = FooSingleton2.class.getDeclaredField("INSTANCE_CREATED");
f.setAccessible(true);
f.set(null, false);
Constructor[] constructors = FooSingleton2.class.getDeclaredConstructors();
Constructor constructor = constructors[0];
constructor.setAccessible(true);
FooSingleton2 spuriousFoo = (FooSingleton2) constructor.newInstance(new Object[0]);

私たちが取ったいかなる防犯措置も迂回される可能性があるので、この案は実行できません.
スタティツクファクトリ
この方法を使用すると、共通のメンバーは静的工場に似ています.
public class FooSingleton3 {
    public final static FooSingleton3 INSTANCE = new FooSingleton3();
    private FooSingleton3() { }
    public static FooSingleton3 getInstance() { return INSTANCE; }
    public void bar() { }
}

getInstance()メソッドは、常に同じオブジェクト参照を返します.この案も反射を防ぐことはできないが、いくつかの利点がある.例えば、APIを変更することなく、一例のインプリメンテーションを変更することができる.getInstance()は、ほとんどの単一インスタンス実装に現れ、これは本当に単一インスタンスモードであることを示しています.
遅延ロードの単一モード
(注:ソフトウェアエンジニアリングでは、Initialization-on-demand holderという習語は、遅延ロードの単一のパターンを指し、ウィキペディアを参照)
可能な限り単一インスタンスの作成(怠け者ロード)を遅らせる場合は、getInstance()メソッドが最初に呼び出されたときにスレッドが安全に単一インスタンスを作成するために、遅延初期化メソッドを使用します.以前のシナリオと比較して、クラスを最初に参照すると、単一の例(餓漢式ロード)が作成されます.これは進歩です.次のようにします.
public class FooSingleton4 {
    private FooSingleton4() {
    }
    public static FooSingleton4 getInstance() {
        return FooSingleton4Holder.INSTANCE;
    }
    private static class FooSingleton4Holder {
        private static final FooSingleton4 INSTANCE = new FooSingleton4();
    }
}

シーケンス化に注意
単一のインスタンスがシーケンス化されると、別の脅威に直面します.したがって、すべてのフィールドをtransientとして宣言し(シーケンス化されないように)、独自のインスタンスINSTANCEの参照を返すカスタムreadResolve()メソッドを提供する必要があります.
列挙
ここでは、一例INSTANCEの容器として列挙する.
public enum FooEnumSingleton {
    INSTANCE;
    public static FooEnumSingleton getInstance() { return INSTANCE; }
    public void bar() { }
}

Java言語仕様8.9によれば、「Enumのfinalクローン法は、列挙が永遠にクローン化されないことを保証し、その特殊なシーケンス化メカニズムは、コピーされたオブジェクトを逆シーケンス化できないことを保証する.また、反射による列挙の実例化も禁止する.この4つの態様は、列挙定数以外に同類の列挙例が存在しないことを保証する」
これにより,シーケンス化,クローン化,反射の攻撃を簡単に防ぐことができるようになった.この話を初めて見て、私はすぐにそれが間違っていることを証明したいと思っています.次のコードに示すように、これらの保護を迂回するのは簡単です.
 Constructor con = FooEnumSingleton.class.getDeclaredConstructors()[0];
 Method[] methods = con.getClass().getDeclaredMethods();
 for (Method method : methods) {
     if (method.getName().equals("acquireConstructorAccessor")) {
         method.setAccessible(true);
         method.invoke(con, new Object[0]);
     }
  }
  Field[] fields = con.getClass().getDeclaredFields();
  Object ca = null;
  for (Field field : fields) {
      if (field.getName().equals("constructorAccessor")) {
          field.setAccessible(true);
          ca = field.get(con);
      }
  }
  Method method = ca.getClass().getMethod("newInstance", new Class[]{Object[].class});
  method.setAccessible(true);
  FooEnumSingleton spuriousEnum = (FooEnumSingleton) method.invoke(ca, new Object[]{new Object[]{"SPURIOUS_INSTANCE", 1}});
  printInfo(FooEnumSingleton.INSTANCE);
  printInfo(spuriousEnum);
}
private static void printInfo(FooEnumSingleton e) {
    System.out.println(e.getClass() + ":" + e.name() + ":" + e.ordinal());
}

このコードを実行すると、次の結果が得られます.
class com.blogspot.minborgsjavapot.singleton.FooEnumSingleton:INSTANCE:0
class com.blogspot.minborgsjavapot.singleton.FooEnumSingleton:SPURIOUS_INSTANCE:1

列挙の欠点はjavaから継承されているため、別のベースクラスから継承できないことです.lang.Enum.この継承をシミュレートするには、別の記事で紹介した混入モード(mixin pattern)を参照してください.
列挙の利点の1つは、後で「2例(dualton)」または「3例(tringleton)」を望む場合は、新しい列挙インスタンスを追加するだけでよいということです.たとえば、1つのインスタンスのキャッシュがある場合、キャッシュに複数の階層を導入したい場合があります.
結論
これらの保護は、単一の例を迂回するのは容易ではないが、確かに万全の方法はない.もっと良い案があれば、教えてください.
列挙は、単一の例示的なモードを実現するための簡単で効率的な方法である.継承や怠け者のロードが必要な場合は、初期化スキームを遅らせるのがいいです.