かとじゅんの技術日誌

技術の話をするところ

ScalaのシングルトンをJavaの視点で読み解く

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は注意深く扱うというのはスレッドセーフの観点からも正しいということなんでしょうね、興味深い。