2020/03/21

Java のシングルトンを正しく実装しよう

Java プログラマの皆さん、シングルトンを正しく実装できますか? 「そんなの簡単だよ」とおっしゃると思いますが、意外と注意点が多いものです。ここでは正しいシングルトンの実装を確認しておきたいと思います。

一番ありがちな実装

シングルトンクラスを実装するとき、こんな風にすることが多いのではないでしょうか。
public final class Singleton {
  private static final Singleton instance = new Singleton();

  private Singleton() {
  }

  public static Singleton getInstance() {
    return instance;
  }
}

プログラムの特性によっては、これはこれでいいと思います。ポイントは以下の通り。
  • 1行目: シングルトンは final class にしなければならない
    • サブクラスを作られると Singleton 型を持つ他のクラスができ、そのインスタンスを作るとシングルトン性が保証されなくなるいため
  • 2行目: インスタンスは static 変数に格納し、クラス初期化時にインスタンスを生成する
    • 遅延初期化したい場合は、次の Initialization-on-demand holder 版を見てください
    • コンストラクタは private にし、外部からインスタンスを生成できないようにする
  • 7行目: getInstance() を呼ぶことでインスタンスを取得できるようにする

遅延初期化したい場合の間違ったアプローチ

前の例では、クラス初期化時にインスタンス生成していますが、これを遅延初期化にしたくなるときってありますよね。でも、意外に難しいのです。

安直に、getInstance() 時に null ならば初期化するように作ると、こうなります。
public final class Singleton {
  private static Singleton instance;

  private Singleton() {
  }

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

一見良さそうですが、これではマルチスレッド・プログラムで問題を生じます。getInstance() が並列実行されたときに、シングルトン性が保証されません。getInstance() を synchronized メソッドにすれば解決しますが、同期化コストで性能が悪化します。

性能劣化を回避する方法として、Double-checked locking という方法が考案されましたが、これは実は正しく機能しません。こちらの記事をご覧ください。

JDK1.5 以降、instance 変数に volatile 修飾子を付けるという選択肢も出てきましたが、volatile のコストもそれなりに高く、遅延初期化をしたいがためだけにそのコストを払うのは割に合いません。

遅延初期化したい場合の正しいアプローチ

正しい解決策は、Initialization-on-demand holder idiom を用いることです。
public final class Singleton {
  private static final class SingletonHolder {
    private static final Singleton instance = new Singleton();
  }

  private Singleton() {
  }

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

Singleton#getIntance() が初めて呼ばれたときに、SingletonHolder クラスへのアクセスが初めて発生し、そのときに SingletonHolder クラスの初期化が実行されます。そこで初めて new Singleton() が実行されますので (3行目)、遅延初期化が実現されたことになります。

見逃しがちなシリアライズの考慮

シングルトン性の保証で見逃しがちなのが、シリアライズの対策です。シリアライズ可能なシングルトンは、単に Serializable インターフェースをつけただけではダメです。シリアライズしてデシリアライズするとインスタンスが増えてしまいます。そのため、以下のような対策が必要です。

import java.io.Serializable;

public final class Singleton implements Serializable {
  private static final long serialVersionUID = 1L;

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

  private Singleton() {
  }

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

  private Object readResolve() {
    return SingletonHolder.instance;
  }
}

ポイントは17行目の readResolve() です。readResolve() は定義しておくとデシリアライズ後に呼ばれることになっている Java 言語仕様で定められたメソッドです。デシリアライズ後にオブジェクトに手を加えるために使うのですが、ここで instance を返すことによってデシリアライズ後のインスタンスをシングルトンとして保持しているものにすりかえてしまいます。こうすることで、インスタンスが増えることを防ぎ、シングルトン性を保証します。

普段からここまで考慮してシングルトンクラスを定義している方も少ないと思いますが、ここまでこだわることが不具合を未然に防ぐことにつながります。是非覚えておいてくださいね。