JPCERT コーディネーションセンター

MSC07-J. シングルトンオブジェクトのインスタンスを複数作らない

シングルトンデザインパターンの目的について、Gamma その他による著作では、以下のように簡潔に説明されている[Gamma 1995]。

あるクラスに対してインスタンスが1つしか存在しないことを保証し、それにアクセスするためのグローバルな方法を提供する。

シングルトンクラスはひとつしか存在しないので、「シングルトンのインスタンスフィールドは、static フィールドと同様、ひとつのクラスに一回だけ生成される。シングルトンは、データベース接続やソケットなどのようなリソースへのアクセスを管理するためによく使われる」[Fox 2001]。他の使用例としては、性能統計データの管理、システムの監視と記録、プリンタスプーラの実装、オーディオファイルが複数同時に再生されないようにする、などがある。static メソッドのみを含むクラスはシングルトンパターンの有力な候補である。

典型的なシングルトンパターンでは、private static クラスフィールドを含むクラスが持つ唯一のインスタンスを使う。このインスタンスは「遅延初期化(lazy initialization)」によって生成することができる。つまり、インスタンスが作られるのはクラスがロードされたときではなく、初めて使われるときである。

シングルトンデザインパターンを実装するクラスは、インスタンスが複数生成されることを防がなければならない。それを実現する手法としては以下が挙げられる。

違反コード (private でないコンストラクタ)

以下の違反コード例では、シングルトンのインスタンスを生成するために、private でないコンストラクタを使っている。

class MySingleton {
  private static MySingleton Instance;

  protected MySingleton() {    
    Instance = new MySingleton();
  }

  public static synchronized MySingleton getInstance() {    
    return Instance;
  }
}

悪意を持ったサブクラスが、コンストラクタのアクセス範囲を protected から public に拡大し、その結果、信頼できないコードによって複数のインスタンスが生成されてしまうかもしれない。さらに、クラスフィールド Instance は final 宣言されてない。

適合コード (private コンストラクタ)

以下の適合コードでは、コンストラクタのアクセス範囲を private にして、さらに、Instance フィールドを即座に初期化している。これによって、Instance を final 宣言できる。シングルトンのコンストラクタは private でなければならない。

class MySingleton {
  private static final MySingleton Instance = new MySingleton();

  private MySingleton() {    
    // コンストラクタを private にして、信頼できない呼び出し元によるインスタンス生成を防止する
  }

  public static synchronized MySingleton getInstance() {    
    return Instance;
  }
}

MySingleton クラスのコンストラクタは private なので、クラス自身を final 宣言する必要はない。

違反コード (スレッド間の可視性)

Singleton クラスの getter メソッドが、シングルトンを必要に応じて初期化し、2つ以上のスレッドによって並行呼び出しされる場合、Singleton クラスの複数のインスタンスが生成されてしまう可能性がある。

class MySingleton {
  private static MySingleton Instance;

  private MySingleton() {    
    // コンストラクタを private にして、信頼できない呼び出し元によるインスタンス生成を防止する
  }

  // 遅延初期化
  public static MySingleton getInstance() { // 同期していない
    if (Instance == null) {
      Instance = new MySingleton();
    }
    return Instance;
  }
}

マルチスレッドプログラムにおいては、シングルトンを初期化するメソッドは、複数のインスタンスが生成されないよう、なんらかの形でロックの仕組みを使わなければならない。

違反コード (不適切な同期)

シングルトンの生成を synchronized ブロックのなかで行ったとしても、複数のインスタンスを生成できてしまう。

public static MySingleton getInstance() {
  if (Instance == null) {
    synchronized (MySingleton.class) {
      Instance = new MySingleton();
    }
  }
  return Instance;
}

複数のインスタンスが生成されてしまう理由は、2つ以上のスレッドが同時に、if の条件式で Instance フィールドの値が null であると見なし、それらのスレッドがひとつずつ synchronized ブロックの処理を行うからである。

適合コード (synchronized メソッド)

複数のスレッドによってシングルトンのインスタンスが複数生成されてしまう問題に対処するために、getInstance() を synchronized メソッドにする。

class MySingleton {
  private static MySingleton Instance;

  private MySingleton() {
    // コンストラクタを private にして、信頼できない呼び出し元によるインスタンス生成を防止する
  }

  // 遅延初期化
  public static synchronized MySingleton getInstance() {
    if (Instance == null) {
      Instance = new MySingleton();
    }
    return Instance;
  }
}
適合コード (ダブルチェックロック手法)

スレッドセーフなシングルトンを実装するもうひとつの方法は、ダブルチェックロック手法を正しく使うことである。

class MySingleton {
  private static volatile MySingleton Instance;

  private MySingleton() {
    // コンストラクタを private にして、信頼できない呼び出し元によるインスタンス生成を防止する
  }

  // ダブルチェックロック
  public static MySingleton getInstance() {
    if (Instance == null) {
      synchronized (MySingleton.class) {
        if (Instance == null) {
          Instance = new MySingleton();
        }
      }
    }
    return Instance;
  }
}

このデザインパターンは正しく実装されないことが多い。ダブルチェックロック手法の正しい実装の方法について詳しくは、「LCK10-J. ダブルチェックロック手法を誤用しない」を参照のこと。

適合コード (Initialize-on-Demand Holder クラスパターン)

以下の適合コードでは、static な内部クラスを使ってシングルトンのインスタンスを生成している。

class MySingleton {
  static class SingletonHolder {
    static MySingleton Instance = new MySingleton();
  }

  public static MySingleton getInstance() {
    return SingletonHolder.Instance;
  }
}

これは、initialize-on-demand holder クラスパターンとして知られている。詳しくは「LCK10-J. ダブルチェックロック手法を誤用しない」を参照。

違反コード (Serializable インタフェース)

以下の違反コード例では、java.io.Serializable インタフェースを実装して、シリアライズできるようにしている。このクラスを復元するということは、複数のインスタンスが生成される可能性があるということを意味している。

class MySingleton implements Serializable {
  private static final long serialVersionUID = 6825273283542226860L;
  private static MySingleton Instance;

  private MySingleton() {
    // コンストラクタを private にして、信頼できない呼び出し元によるインスタンス生成を防止する
  }

  // 遅延初期化
  public static synchronized MySingleton getInstance() {
    if (Instance == null) {
      Instance = new MySingleton();
    }
    return Instance;
  }
}

シングルトンのコンストラクタに、インスタンス生成は一回しか行わないように制約を設けても無駄である。復元処理ではコンストラクタは使われない。

違反コード (readResolve() メソッド)

シングルトンの性質を保つには、元のインスタンスを返すような readResolve() メソッドを追加するだけでは不十分である。また、すべてのフィールドが transient あるいは static と宣言されても、やはりセキュアでない。

class MySingleton implements Serializable {
  private static final long serialVersionUID = 6825273283542226860L;
  private static MySingleton Instance;

  private MySingleton() {
    // コンストラクタを private にして、信頼できない呼び出し元によるインスタンス生成を防止する
  }

  // 遅延初期化
  public static synchronized MySingleton getInstance() {
    if (Instance == null) {
      Instance = new MySingleton();
    }
    return Instance;
  }

  private Object readResolve() {
    return Instance; 
  }
}

攻撃者は、細工したシリアライズされたストリームを読み込むようなクラスを自分でつくり、実行時に追加することができる。

public class Untrusted implements Serializable {
  public static MySingleton captured;
  public MySingleton capture;
  
  public Untrusted(MySingleton capture) {
    this.capture = capture;
  }

  private void readObject(java.io.ObjectInputStream in)
                          throws Exception {
    in.defaultReadObject();
    captured = capture;
  }
}

以下のクラスをシリアライズすることで細工したストリームをつくることができる。

public final class MySingleton
                   implements java.io.Serializable {
  private static final long serialVersionUID =
      6825273283542226860L;
  public Untrusted untrusted =
      new Untrusted(this); // 追加のフィールド
 
  public MySingleton() { }
}

復元時には、MySingleton.readResolve() が呼び出されるより前に MySingleton.untrusted フィールドが復元される。そのため、Untrusted.captured には、MySingleton.Instance ではなく、細工されたストリームから復元されたインスタンスが代入される。攻撃者が、クラスを追加することで、シリアライズ可能なシングルトンクラスに対する攻撃を行うことができる場合、これは致命的な問題となる。

違反コード (transient でないインスタンスフィールド)

以下の Serializable を実装したコード例は、transient でないインスタンスフィールド str を使っている。

class MySingleton implements Serializable {
  private static final long serialVersionUID =
      2787342337386756967L;
  private static MySingleton Instance;
  
  // transient でないインスタンスフィールド
  private String[] str = {"one", "two", "three"}; 
                 
  private MySingleton() {
    // コンストラクタを private にして、信頼できない呼び出し元によるインスタンス生成を防止する
  }

  public void displayStr() {
    System.out.println(Arrays.toString(str));
  }
 
  private Object readResolve() {
    return Instance;
  }
}

「もし、シングルトンが transient でないオブジェクト参照フィールドを含んでいると、シングルトンの readResolve メソッドが実行される前に、そのフィールドの内容が復元される。これにより、オブジェクト参照フィールドの内容が復元された時点で、注意深く作られたストリームが、元の復元されたシングルトンへの参照を『盜む』ことを可能にする」[Bloch 2008]

適合コード (列挙型)

状態を持ったシングルトンクラスはシリアライズ可能にしてはいけない。予防策として、シリアライズ可能なクラスは、transient でない、あるいは static でないインスタンス変数にシングルトンオブジェクトへの参照を保持しないこと。こうすれば、シングルトンが間接的にシリアライズされることを防げる。

Bloch は、シングルトンをシリアライズ可能にする必要がある場合、従来の実装の代わりに列挙型を使うことを勧めている[Bloch 2008]。

public enum MySingleton {
  ; // 空のenum値のリスト

  private static MySingleton Instance;

  // transient でないフィールド
  private String[] str = {"one", "two", "three"};

  public void displayStr() {
    System.out.println(Arrays.toString(str));
  }	 
}

このアプローチは、広く行われている実装方法と機能的には同等であるが、より安全である。オブジェクトインスタンスがひとつだけ存在することを保証するとともに、シリアライズに関する性質を提供している。(java.lang.Enum<E>java.io.Serializable を拡張している。)

違反コード (cloneable シングルトン)

シングルトンクラスが java.lang.cloneable を直接実装している場合や継承している場合、clone() メソッドを使ってシングルトンの複製を作ることが可能である。以下の違反コード例のシングルトンは、java.lang.cloneable インタフェースを実装している。

class MySingleton implements Cloneable {
  private static MySingleton Instance;

  private MySingleton() {
    // コンストラクタを private にして、
    // 信頼できない呼び出し元によるインスタンス生成を防止する
  }

  // 遅延初期化
  public static synchronized MySingleton getInstance() {
    if (Instance == null) {
      Instance = new MySingleton();
    }
    return Instance;
  }
}
適合コード (clone() メソッドをオーバーライドする)

シングルトンクラスを cloneable にしてはならない。そのためには、Cloneable インタフェースを実装しないこと。また、すでに cloneable インタフェースを実装しているクラスのサブクラスとしてシングルトンクラスを定義しないこと。

継承によって間接的に cloneable インタフェースを実装しなければならないときは、オブジェクトの clone() メソッドを、CloneNotSupportedException 例外をスローするようなメソッドでオーバーライドしなければならない[Daconta 2003]。

class MySingleton implements Cloneable {
  private static MySingleton Instance;

  private MySingleton() {
    // コンストラクタを private にして、信頼できない呼び出し元によるインスタンス生成を防止する
  }

  // 遅延初期化
  public static synchronized MySingleton getInstance() {
    if (Instance == null) {
      Instance = new MySingleton();
    }
    return Instance;
  }

  public Object clone() throws CloneNotSupportedException {
    throw new CloneNotSupportedException();
  }
}

clone() メソッドの誤用を防ぐ方法に関する詳細は、「OBJ07-J. センシティブなクラスはコピーさせない」を参照のこと。

違反コード (ガベージコレクション)

到達不可能になったクラスはガベージコレクタによって回収されるかもしれない。プログラムが動作している全期間に渡ってシングルトンの性質を維持し続けなければならない場合、このような挙動は問題となる可能性がある。

static なシングルトンをロードしたクラスローダが回収可能になると、static なシングルトン自身も回収可能となる。このような状況は、標準でない独自のクラスローダを使ってシングルトンをロードした場合に発生する。以下の違反コード例では、異なるスコープに存在するシングルトンオブジェクトのハッシュコードとして、異なる値が出力される。

{
  ClassLoader cl1 = new MyClassLoader();
  Class class1 = cl1.loadClass(MySingleton.class.getName());
  Method classMethod = 
      class1.getDeclaredMethod("getInstance", new Class[] { });
  Object singleton = classMethod.invoke(null, new Object[] { });
  System.out.println(singleton.hashCode());
}

ClassLoader cl1 = new MyClassLoader();
Class class1 = cl1.loadClass(MySingleton.class.getName());
Method classMethod = 
    class1.getDeclaredMethod("getInstance", new Class[] { });
Object singleton = classMethod.invoke(null, new Object[] { } );
System.out.println(singleton.hashCode());

このコードでは、元のインスタンスを使うことが要請されているのだが、スコープ外のコードは別のインスタンスを生成できてしまう。

シングルトンのインスタンスは、それをロードしたクラスローダと関連付けられており、JVM 全体で見ると、同一のシングルトンクラスであっても(異なるクラスローダを使うことで)複数のインスタンスをつくることができる。このような状況は J2EE コンテナやアプレットで発生する。技術的な観点では、これらのインスタンスは別々のクラスのインスタンスと見なされる。プログラムの要件にも依存するが、上記のような複数のインスタンスの生成を許す状況はセキュアでないかもしれない。

適合コード (ガベージコレクションを防ぐ)

前述のガベージコレクションの問題を考慮した適合コードを以下に示す。クラスは、そのクラスをロードした ClassLoader オブジェクトが回収可能になるまでは、回収可能にならない。そのため、ガベージコレクションを防ぐ単純な方法は、回収されないようにしたいシングルトンオブジェクトに対する直接参照あるいは間接参照を、実行中のスレッドに持っておくことである。

この方法を実装したのが以下の適合コードである。どのスコープにおいても、一貫したハッシュコードを出力する。このコードでは、「TSM02-J. クラスの初期化中にバックグラウンドスレッドを使用しない」で説明している ObjectPreserver クラスを使っている[Grand 2002]。

{
  ClassLoader cl1 = new MyClassLoader();
  Class class1 = cl1.loadClass(MySingleton.class.getName());
  Method classMethod = 
      class1.getDeclaredMethod("getInstance", new Class[] { });
  Object singleton = classMethod.invoke(null, new Object[] { });
  ObjectPreserver.preserveObject(singleton); // オブジェクトを保存
  System.out.println(singleton.hashCode());
}

ClassLoader cl1 = new MyClassLoader();
Class class1 = cl1.loadClass(MySingleton.class.getName());
Method classMethod = 
    class1.getDeclaredMethod("getInstance", new Class[] { });
// 保存しておいたオブジェクトを取り出す
Object singleton = ObjectPreserver.getObject();  
System.out.println(singleton.hashCode());
リスク評価

シングルトンデザインパターンを間違った形で使うと、複数のインスタンスが生成される。これは、シングルトンの性質に反する。

ルール 深刻度 可能性 修正コスト 優先度 レベル
MSC07-J P2 L3
関連ガイドライン
MITRE CWE CWE-543. Use of Singleton pattern without synchronization in a multithreaded context
参考文献
[Bloch 2008] Item 3. Enforce the singleton property with a private constructor or an enum type; and Item 77. For instance control, prefer enum types to readResolve
[Daconta 2003] Item 15. Avoiding singleton pitfalls
[Darwin 2004] 9.10 Enforcing the Singleton Pattern
[Fox 2001] When Is a Singleton Not a Singleton? 
[Gamma 1995] Singleton
[Grand 2002] Chapter 5, Creational Patterns, Singleton
[JLS 2005] Chapter 17, Threads and Locks
翻訳元

これは以下のページを翻訳したものです。

MSC07-J. Prevent multiple instantiations of singleton objects (revision 132)

Top へ

Topへ
最新情報(RSSメーリングリストTwitter