本エントリーはJava Advent Calendar 2025の6日目です。
一昨日はLazy Constantの使い方を紹介したので、今日はLazy Constantがどうやって実装されているかを紹介していきます。
Lazy Constant
https://www.javainthebox.com/2025/12/lazy-constant.html
シングルトン
いきなりシングルトンと言われても... という感じだとは思いますが、Lazy Constantの実装を紐解く前に、シングルトンについて考えてみます。
シングルトンはデザインパターンの1つで、インスタンスを1つに制限するためのパターンです。
シングルトンは一種の大域変数になってしまうので、使いすぎるのはよくないですし、最近はほとんど使われなくなったように思います。でも、その実装はLazy Constantにつながるのです。
そもそもシングルトンの実装ってどうなっていたか覚えていらっしゃいますでしょうか?
シンプルなシングルトン
まずはシンプルなシングルトンの実装です。
シングルトンは自身のインスタンスをstaticフィールドで1つ保持します。そして、インスタンスを取得するメソッドが初めてコールされた時に、インスタンスを生成します。インスタンスが存在すれば、それを返します。つまり、遅延初期化なのです。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton get() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
インスタンスを勝手に作られないように、コンストラクターはprivateで宣言します。
getメソッド(getInstanceメソッドのことも多いですが、ここではgetメソッドにします)では、staticフィールドのinstanceがnullだったら、つまり初期化されていなければSingletonインスタンスを生成します。そして、そのinstanceを返します。
シングルトンにデータを持たせるのであれば、それなりにフィールドなどを定義しますが、インスタンスを1つに限定させるための実装としてはこれでOKです。
しかも、実際にシングルトンのインスタンスを実際に使う時まで、その初期化を遅らせることができます。
しかし、問題もあります。この実装はスレッドセーフではないという点です。シングルスレッドであればいいのですが、マルチスレッドでは使えません。
スレッドセーフなシングルトン
スレッドセーフにするにはどうすればよいでしょう?
もっとも単純なのは、getメソッドをsynchronizedにするという方法です。
public class Singleton {
private static Singleton instance;
private Singleton() {}
public synchronized static Singleton get() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
これでマルチスレッドでも動作します。
問題はスケールしないということです。スレッド数が2, 3であればよいのですが、スレッド数が増えるととたんにロック待ちで渋滞してしまいます。
instanceフィールドは初期化した後は参照されるだけなので、本来であれば同期化は必要ありません。問題は未初期化の状態時に複数スレッドからgetメソッドをコールされる場合です。
そこで、getメソッドのインスタンス生成のところだけ同期化することを考えてみます。
public static Singleton get() {
if (instance == null) {
// NG これはダメな実装
synchronized(instance) {
instance = new Singleton();
}
}
return instance;
}
}
しかし、これではダメなのです。
CPUの効率化のために命令を入れ替えたり、キャッシュにinstanceの値が残っていることもあるので、instanceを初期化しているときに複数のスレッドからアクセスされてしまうことがあります。
そこで、出てくるのがダブルチェックロックという方法です。でも、単にダブルチェックロックだけでは、キャッシュ上のinstanceとメモリのinstanceが同期化されているか保証されません。
そこで、instanceフィールドをアトミックに処理されるようにします。
厳密にやるのであればjava.util.concurrent.atomic.AtomicReferenceクラスを使用しますが、volatileでも大丈夫です。
最終的には次のようになります。
public class Singleton {
// volatileで宣言することによりアトミック性を保証する
private static volatile Singleton instance;
private Singleton() {}
public static Singleton get() {
if (instance == null) {
synchronized (Singleton.class) {
// ダブルチェック
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
これでマルチスレッドでスケールするスレッドセーフなシングルトンになります。
Lazy Constant
シングルトンが理解できたとして、Lazy Constantの実装です。
java.lang.LazyConstantはインタフェースなので、実際の遅延初期化の部分は実装されていません。どこで実装されているかというと、LazyConstantインタフェースのstaticメソッドのofメソッドを見れば分かります。
static <T> LazyConstant<T> of(Supplier<? extends T> computingFunction) {
Objects.requireNonNull(computingFunction);
if (computingFunction instanceof LazyConstant<? extends T> lc) {
return (LazyConstant<T>) lc;
}
return LazyConstantImpl.ofLazy(computingFunction);
}
return文のところを見ると、LazyConstantImplクラスというのがあることがわかります。これがLazyConstantインタフェースを実装したコンクリートクラスです。パッケージはjdk.internal.langパッケージで、公開されていないクラスだということが分かります。
では、遅延初期化を行うgetメソッドを見る前に、クラスの宣言と関連するフィールド、そしてofLazyメソッドを見ておきましょう。この他にもフィールドありますが、関連するところだけ。
@AOTSafeClassInitializer
public final class LazyConstantImpl<T> implements LazyConstant<T> {
@Stable
private T constant;
@Stable
private volatile Supplier<? extends T> computingFunction;
private LazyConstantImpl(Supplier<? extends T> computingFunction) {
this.computingFunction = computingFunction;
}
public static <T> LazyConstantImpl<T> ofLazy(Supplier<? extends T> computingFunction) {
return new LazyConstantImpl<>(computingFunction);
}
見慣れないアノテーションが使われていますが、Leyden関連や最適化のヒントになるアノテーションです。
ここで注目しておいていただきたいのが、値を保持するconstantフィールドがvolatileではないということです。そして、ofメソッドの引数のラムダ式はcomputingFunctionフィールドで保持しています。
では、保持している値を返すgetメソッドを見ていきましょう。
@ForceInline
@Override
public T get() {
final T t = getAcquire();
return (t != null) ? t : getSlowPath();
}
private T getSlowPath() {
preventReentry();
synchronized (this) {
T t = getAcquire();
if (t == null) {
t = computingFunction.get();
Objects.requireNonNull(t);
setRelease(t);
// Allow the underlying supplier to be collected after successful use
computingFunction = null;
}
return t;
}
}
getメソッドの最初に出てくるgetAcquireメソッドはconstantフィールドを取得するメソッドです。このメソッドについては後でもう一度触れます。
getAcquireメソッドでconstantフィールドをローカル変数のtに代入しています。続いて、tがnullでなければ、tをそのまま返しています。逆に、tがnullならばgetSlowPathメソッドをコールしています。
getSlowPathメソッドの先頭でpreventReentryメソッドをコールしていますが、これはロックを取得している状態で再びロックを取得する(これを再入すると呼びます)ことを防ぐメソッドです。Javaのsynchronizedは再入が可能なロックなのですが、ここではそれを防いでいるということです。
そして、synchronizedでロックを取得し、再びtを取得してnullかどうかをチェックしています。つまりダブルチェックになっているということです。
ダブルチェックをしてtがnullの場合、computingFunctionフィールドに対してgetメソッドをコールしています。これがSupplierのラムダ式の実行を意味しています
つまり、ここで値の初期化を行っています。
初期化した値がnullの場合はrequireNonNullメソッドでチェックしてNullPointerException例外をスローします。これが、前回のエントリーで例外を扱う場合の1の選択肢(ラムダ式でnullを返す)時の挙動になります。
次のsetReleaseメソッドはgetAcquireメソッドの逆です。
そして、computingFunctionにnullを代入しています。つまり、一度Supplierのラムダ式が実行されたら、その後はラムダ式を実行することができないということです。
このようにして、ダブルチェックロックを使って値の初期化を行っています。
しかし、気になるのは、シングルトンでは遅延初期化するフィールドがvolatileだったのにLazyConstantImplクラスではvolatileではないという点です。
この問題はgetAcquireメソッドを見てみれば、理由が分かります。
@SuppressWarnings("unchecked")
@ForceInline
private T getAcquire() {
return (T) UNSAFE.getReferenceAcquire(this, CONSTANT_OFFSET);
}
ここで使われているUNSAFE変数は、一般には使わないようにと言われている危険なjdk.internal.misc.Unsafeクラスです。標準ライブラリだからこそ、使えるということですね。
そして、UnsafeクラスのgetReferenceAcquireメソッドを見てみると...
@IntrinsicCandidate
public final Object getReferenceAcquire(Object o, long offset) {
return getReferenceVolatile(o, offset);
}
なんと参照をvolatileで取得するメソッドをコールしていました。
つまり、LazyConstantImplクラスのconstantフィールドはvolatileで定義されてはいないものの、アクセスする時はvolatile相当で行われるということです。
これがconstantフィールドがvolatileで定義されていない理由になります。まぁ、普通にはできない技ですね(そもそもUnsafeクラスは使えないですし)。
もう1つ標準ライブラリだからこその技が最適化のヒントとなるアノテーションです。
たとえば、@ForceInlineアノテーションはメソッドのインライン化を行わせるアノテーションです。また、@Stableアノテーションは値が変更されないことを保証して、値を埋め込むなどの最適化を可能にしています。
このような最適化に対するアノテーションを使うことで、実行時最適化をやりやすくしているわけです。
まとめ
finalフィールドの遅延初期化を行うLazy Constantの実装を見てきました。
遅延初期化で使われている手法は、シングルトンで使われていたvolatileとダブルチェックロックです。この手法を使う場面というのはなかなかないとは思いますが、知識として知っておくのはよいですね。
そもそも、Lazy Constant自体がそれほど頻繁に使われるAPIではありません。しかし、もしイミュータブルなクラスで遅延初期化をしなければならないような場合はぜひ思い出してやってください。
0 件のコメント:
コメントを投稿