Scalaでシングルトンといえば、object型でしょう。実は、「はい それで終わり」ではありません。今日はそんな話。
object Singleton { println("Construct") val name = "SINGLETON" }
println(Singleton.name)
jadるとこんな感じ。
public final class Singleton$ implements ScalaObject { private final String name = "SINGLETON"; public String name() { return name; } public static final Singleton$ MODULE$ = this; static { new Singleton$(); } // 上記は public static final Singleton$ MODULE$ = new Singleton$(); だと思われ。 private Singleton$() { Predef$.MODULE$.println("Construct"); } }
public final class Singleton { public static final String name() { return Singleton$.MODULE$.name(); } }
// println(Singleton.name)
Predef$.MODULE$.println(Singleton$.MODULE$.name());
言わずもがな、object型で宣言したシングルトンは、static finalフィールドでインスタンスを保持するので、並行処理においての初期化安全性が保証されるわけです。
Scalaで遅延初期化のシングルトンを書くことはできるか
以下のように書いてみました。(シングルトンというか、lazy valの話になってしまいますが、、、)
遅延初期化するシングルトンクラスとして、LazySingletonクラスを用意し、コンパニオンオブジェクトであるLazySingletonオブジェクトを同一ファイルに定義します。
class LazySingleton { println("Lazy Construct") val name = "SINGLETON" } object LazySingleton { private lazy val instance = new LazySingleton def apply() = instance }
利用する時は以下のような感じ。
// 以下はprintln(LazySingleton.apply().name)の構文糖衣 println(LazySingleton().name) // ここで初期化される println(LazySingleton().name)
LazySingletonオブジェクトのlazy valであるinstanceフィールドが参照される時にLazySingletonクラスのコンストラクタが呼ばれて、遅延初期化されます。
jadると以下のようなコードになります。
public final class LazySingleton$ implements ScalaObject { public static final LazySingleton$ MODULE$ = this; static { new LazySingleton$(); } private LazySingleton instance; public volatile int bitmap$0; // 初期化フラグだと思われ private LazySingleton instance() { if((bitmap$0 & 1) == 0) synchronized(this) { if((bitmap$0 & 1) == 0) { instance = new LazySingleton(); bitmap$0 = bitmap$0 | 1; } BoxedUnit _tmp = BoxedUnit.UNIT; } return instance; } public LazySingleton apply() { return instance(); } private LazySingleton$() { } }
// println(LazySingleton().name)
Predef$.MODULE$.println(LazySingleton$.MODULE$.apply().name());
注目したいのは、lazy valのセマンティクスですね。instanceメソッドを見てください。初期化フラグと思われるvolatileフィールドであるbitmap$0フィールドを使ったダブルチェッキングロジックになっています。
対策3:volatileフィールドと固有ロック(synchronized)を併せて使う
次は、volatileとsynchronizedを使ったダブルチェッキングロジック
つまり、Scalaでは、finalフィールドとなるvalだけでなく、lazy valもスレッドセーフのですね。 意識しなくてもスレッドセーフになっているってすごいネ。varは注意深く扱うというのはスレッドセーフの観点からも正しいということなんでしょうね、興味深い。