コード型ログ(4) Initialization-on-demand Holder

前回: コード型ログ(3) privateなメソッドのテスト - 超ウィザード級ハッカーのたのしみ


必要なときにインスタンスを作ったら、メモリ効率がよくないかと思って変なことをしてしまう悪い例。

class BadSingleton1 {
  private static BadSingleton1 instance;

  // 他の人が勝手にインスタンスを作らないように private にする。
  private BadSingleton1() { };

  static BadSingleton1 getInstance() {
    // 同時に2つのスレッドが null チェックをするとユニークでなくなる。
    if (instance == null) {
      instance = new BadSingleton1();
    }
    return instance;
  }
}

上のようなことをしたければ排他を取る必要がある。しかし、これでは遅い。

class NotBadSingleton1 {
  private static NotBadSingleton1 instance;

  private NotBadSingleton1() { };

  // ユニーク性は保証されるが、同期コストがかかる。
  synchronized static NotBadSingleton1 getInstance() {
    if (instance == null) {
      instance = new NotBadSingleton1();
    }
    return instance;
  }
}

だから、以下のDouble-checked lockingというものが考案された。しかし、実は正しく排他されていないことが知られている。この場合はインスタンス変数がないが、ある場合にはスレッドが競合したときに、getInstance()で返ってくるインスタンスインスタンス変数が初期化されていない可能性がある。

class BadSingleton2 {
  private static BadSingleton2 instance;

  private BadSingleton2() { };

  static BadSingleton2 getInstance() {
    // instance != null だとしても、インスタンスの初期化が終わっていない
    // 可能性が実はある。
    if (instance == null) {
      synchronized (BadSingleton2.class) {
        if (instance == null) {
          instance = new BadSingleton2();
        }
      }
    }
    return instance;
  }
}

初期化を遅延させたかったら、Initialization-on-demand Holderという名前がついている以下の書き方をする。

class GoodSingleton1 {
  private static class Holder {
    private static GoodSingleton1 instance = new GoodSingleton1();
  }

  private GoodSingleton1() { };

  static GoodSingleton1 getInstance() {
    // Holder クラスが初めて呼ばれたときに、instance は生成され
    // ユニークであることは保証される。
    return Holder.instance;
  }
}

しかし、凝ったことをせずに普通に書いたらいい。

class GoodSingleton2 {
  private static GoodSingleton2 instance = new GoodSingleton2();

  private GoodSingleton2() { };

  static GoodSingleton2 getInstance() {
    return instance;
  }
}

Initialization-on-demand Holderを覚えたからと使いたがる人のコードなんて読みたくない。(私もきっとそういうことをやってるけれども……使わないと覚えないし……)