遅延初期化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インスタンスの参照を得る(重複して生成することはない)というわけです。
巧みですねー。
参考: