Java でのシングルトンの実装の難しさ

伊谢尔伦
リリース: 2016-12-05 11:34:30
オリジナル
1029 人が閲覧しました

シングルトン パターンを実装する簡単で効率的な方法はありますが、あらゆる状況下でシングルトンの整合性を保証する方法はありません。

シングルトン パターンは、クラスが 1 回だけインスタンス化され、グローバルまたはシステム全体のコンポーネントを表すために使用されることを意味します。シングルトン パターンは、ロギング、ファクトリ、ウィンドウ マネージャー、プラットフォーム コンポーネント管理などによく使用されます。シングルトンパターンは一度実装すると変更やオーバーロードが難しく、テストケースの書き方の難しさやコード構造の悪さなどの問題が発生するため、できるだけ避けるべきだと思います。また、次の記事のシングルトン パターンは安全ではありません。

シングルトン パターンをより適切に実装する方法の研究に多くのエネルギーが費やされていますが、シンプルで効率的な実装方法があります。ただし、あらゆる状況下でシングルトンの整合性を保証する唯一の方法はありません。以下をお読みになり、同意するかどうかを確認してください。

最終フィールド

このメソッドはコンストラクターをプライベート化し、パブリック静的最終オブジェクトを提供します:

public class FooSingleton {    
    public final static FooSingleton INSTANCE = new FooSingleton();   
    private FooSingleton() { }    
    public void bar() { }}
ログイン後にコピー

クラスがロードされると、静的オブジェクトが初期化され、この時点でプライベート コンストラクターが最初と最後の転送に使用されます。クラスが初期化される前に複数のスレッドがこのクラスを呼び出した場合でも、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 というイディオムは遅延読み込みシングルトン モードを指します。Wikipedia を参照してください)

できるだけ遅延させたい場合は、シングルトンを作成するには(遅延読み込み)、getInstance() メソッドが初めて呼び出されたときに、遅延初期化メソッドを使用してシングルトンをスレッドセーフに作成できます。クラスが初めて参照されるときにシングルトンを作成する以前のソリューション (ハングリー スタイルのロード) と比較すると、これは改善です。次のとおりです:

public class FooSingleton4 {    
private FooSingleton4() {    
}    
public static FooSingleton4 getInstance() {       
 return FooSingleton4Holder.INSTANCE;    
 }    
private static class FooSingleton4Holder {       
 private static final FooSingleton4 INSTANCE = new FooSingleton4();    
 }
}
ログイン後にコピー

シリアル化には注意してください

シングルトンがシリアル化を実装すると、別の脅威に直面します。したがって、すべてのフィールドを一時的として宣言し (シリアル化されないように)、一意のインスタンス INSTANCE への参照を返すカスタム readResolve() メソッドを提供する必要があります。

列挙

ここでは、単一インスタンス INSTANCE のコンテナとして列挙を使用します:

public enum FooEnumSingleton {
    INSTANCE;    
    public static FooEnumSingleton getInstance() { 
    return INSTANCE; 
    }    
    public void bar() { }
}
ログイン後にコピー

Java 言語仕様 8.9 によると、「Enum の最終的な複製メソッドは列挙が決して複製できないことを保証し、その特別なシリアル化メカニズムは列挙が複製できないことを保証します」同時に、列挙型をインスタンス化するためにリフレクションを使用することも禁止されています。これにより、列挙型定数の外に他の同様の列挙型インスタンスが存在しないことが保証されます。そして反射攻撃。この一節を初めて見たとき、私はすぐにそれが間違いであることを証明したいと思いました。次のコードに示すように、これらの保護を回避するのは簡単です:

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.この種の継承をシミュレートしたい場合は、私の他の記事で紹介したミックスイン パターンを参照してください。

列挙型の利点の 1 つは、後で「デュアルトン」または「トリングルトン」が必要になった場合に、新しい列挙型インスタンスを追加するだけで済むことです。たとえば、シングルトン キャッシュを作成した後、そのキャッシュに複数のレイヤーを導入することもできます。

結論

シングルトンに対するこれらの保護をバイパスするのは簡単ではありませんが、確実な解決策は実際にはありません。より良い解決策があれば、お気軽にお知らせください。

列挙は、シングルトン パターンを実装する簡単かつ効率的な方法です。継承または遅延ロードが必要な場合は、遅延初期化が良い選択です。

シングルトンの幸運を祈ります!

関連ラベル:
ソース:php.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート