読者です 読者をやめる 読者になる 読者になる

シングルトンパターンを復習しよう

今日の一歩

目的

遅延初期化ホルダークラスイデオムを最近知ったので、良い機会なので復習してみる。

勉強内容

  • シングルトンにする意味
  • 遅延初期化する?しない?
  • 遅延初期化ホルダークラスイデオム(オンデマンド初期化ホルダークラスイデオム)について
  • おまけ:phpJavaの違い
シングルトンにする意味
  • 不要なインスタンスを作らないこと
    • 生成処理が一度しか行われないのでコストを抑えられます
    • メモリの節約に繋がります
  • 開発者が見た時に「インスタンスが一つしか生成されない」ことを明示的に表す
    • 開発者の負担を減らす。よけいなことを考えなくていい
遅延初期化する?しない?

一般的なシングルトンのコードは次のようになるでしょう。

public class Singleton {
  private static final Singleton instance = new Singleton();
  private Singleton(){}
  public static Singleton getInstance() {
    return instance;
  }
}

これで問題ないのですが、以下のような理由から遅延初期化を行いたくなるときがあります。

  1. そのフィールドの初期化に結構なコストがかかるとき
  2. 特定の条件でしか参照されないとき
  3. かっこつけたいとき

そうするとフィールド宣言時ではなく、必要とされたときに初期化しようとします。

public class Singleton {
  private static Singleton instance;
  private Singleton(){}
  public static synchronized Singleton getInstance() {
    if(instance == null) {
      instance = new Singleton();
    }
    return instance;
  }
}

ここではメソッドを同期(synchronized)しました。
そうでないやり方はダブルチェックロッキングと合わせて、別日にまとめようと思います。

で、さておきEffective Java第2版では次のように記されています。

ほとんどの最適化の場合と同様に、遅延初期化に対する最高の助言は、「必要でなければするな」です。
遅延初期化はもろ刃の剣です。
(中略)
遅延初期化は(多くの「最適化」と同様に)実際にはパフォーマンスを悪くする可能性があります。
(中略)
複数のスレッドが存在する状況では、遅延初期化は面倒です。
(中略)
ほとんどの状況では、遅延初期化よりは普通の初期化が望ましいです。

[Effective Java第2版 第10章 項目71 遅延初期化を注意して使用する]

「必要でなければするな」、いい言葉です。。。

遅延初期化ホルダークラスイデオム(オンデマンド初期化ホルダークラスイデオム)について

とはいえ遅延初期化をしたい場合、それがstaticフィールドならば次のような書き方で対応出来ます。

public class Singleton {
  private Singleton(){}
  public static Singleton getInstance() {
    return SingletonHolder.instance;
  }

  private static class SingletonHolder {
    static final Singleton instance = new Singleton();
  }
}

この場合、getInstanceが呼ばれるまではSingletonHolderは初期化されません。
Effective Javaに書いてありますが、これは同期不要です。それでいて遅延されています。
これを遅延初期化ホルダークラスイデオム、またはオンデマンド初期化ホルダークラスイデオムと呼ぶようです。
かっこいい!

...本当に?ちょっとだけ確認してみます。

public class Singleton {
  static {
    System.out.println("Singleton Static Initializer!");
  }
  private Singleton(){
    System.out.println("Singleton Construtcor!");
  }
  public static Singleton getInstance() {
    System.out.println("Singleton.getInstance!");
    return SingletonHolder.instance;
  }

  private static class SingletonHolder {
    static {
      System.out.println("SingletonHolder Static Initializer!");
    }
    static final Singleton instance = new Singleton();
  }
}

確認用にところどころに標準出力を仕込んでいます。
これを以下のmainクラスから実行してみます。

public class Main {
  public static void main(String[] args) {
    System.out.println(Singleton.getInstance());
    System.out.println(Singleton.getInstance());
  }
}
Singleton Static Initializer!
Singleton.getInstance!
SingletonHolder Static Initializer!
Singleton Construtcor!
foo.Singleton@137bd6a1
Singleton.getInstance!
foo.Singleton@137bd6a1

1. Singletonクラスがロードされてstaticイニシャライザが実行されます
2. Singleton.getInstanceの呼び出しが実行されます
3. SingletonHolderクラスがロードされてstaticイニシャライザが実行されます
4. SingletonHolder.instanceの初期化によりnew Singleton()が実行されます
5. 2回目以降のSingleton.getInstanceではもちろん初期化は実行されず、上記インスタンスを返すだけです

期待通りに動きました。かっこいい!

おまけ:PHPJavaの違い

Javaでは上記のように書くシングルトンですが、PHPではこう書きます。

<?php
class Singleton {
  private static $instance = null;
  private function __construct(){}
  public static function getInstance() {
    if (is_null(self::$instance)) {
      self::$instance = new self;
    }
    return self::$instance;
  }
}

自身のstaticな変数を参照するのに「self::$変数名」と書かなくてはいけないダサさはさておき。
PHPではこのように遅延初期化な書き方が一般的だと思います。
なぜなら、変数の初期化でnew演算子を使えないから!!

宣言時に初期値を設定することもできますが、初期値は定数値でなければなりません。 つまり、コンパイル時に評価可能な値でなければならず、 実行時の情報がないと評価できない値であってはいけないということです。

Apache MPMをpreforkでPHPで動かしていれば、共有リソースでも扱わない限りマルチスレッドに悩まされることはないだろうからこれでもいいんですけど。
(Javaにおける遅延初期化パターンの問題はマルチスレッドにおける対応の複雑さだと思っているので、そもそもマルチスレッドで動かないのであれば問題にならない)

感想

なんにせよ、遅延初期化ホルダークラスイデオム美しいね!