遅延初期化Singleton

「Javaによる関数型プログラミング」を読んで、学び多かったんですが、そのひとつ、遅延初期化Singletonの実装をメモしておきます。

Singletonインスタンスをオンデマンドで作成するのは案外難しくて、ダブルチェックロッキングは壊れていたりするわけですが、この本で紹介されている方法はうまいことやっています。

public class HeavyHolder {
  private Supplier<Heavy> heavySupplier =
     ()-> createAndCacheHeavy();
  public Heavy getHeavy() {
    return heavySupplier.get();
  }
  private synchronized Heavy createAndCacheHeavy() {
    class ActualHeavyHolder implements Supplier<Heavy> {
      private final Heavy heavyInstance = new Heavy();
      public Heavy get() {
        return heavyInstance;
      }
    }
    if (!ActualHeavyHolder.class.isInstance(heavySupplier)) {
      heavySupplier = new ActualHeavyHolder();
    }
    return heavySupplier.get();
  }
}

最初にこのコードを見た時には、HeavyHolder.getHeavy()はsynchronizedではないので、heavySupplierは同期化されず意図通り動かないのでは?と思いましたが…。

最初にこのクラスのインスタンスを生成したスレッドはheavySupplierに束縛されたラムダを実行し、synchronizedなcreateAndCacheHeavy()の中でActualHeavyHolderのインスタンスを生成し、heavySupplierを書き換えます。createAndCacheHeavy()を出るときにメインメモリ上のheavySupplierは書き換えられます。

後から来たスレッドが呼ぶHeavyHolder.getHeavy()はsynchronizedでないので、キャッシュの古いheavySupplierの値を見るかもしれません。その場合、ラムダを実行しますが、そこで呼ぶcreateAndCacheHeavy()はsynchronizedですからブロックに入るときキャッシュは無効化され、メインメモリのheavySupplierの値を見ます。ここで先ほど設定されていたActualHeavyHolderのインスタンスの参照が見え、生成済のシングルトンHeavyインスタンスの参照を得る(重複して生成することはない)というわけです。

巧みですねー。

参考: