2025/12/06

Lazy Constant 実装編

このエントリーをはてなブックマークに追加

本エントリーは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ではありません。しかし、もしイミュータブルなクラスで遅延初期化をしなければならないような場合はぜひ思い出してやってください。

2025/12/04

Lazy Constant

このエントリーをはてなブックマークに追加

本エントリーはJava Advent Calendar 2025の4日目です。昨日はAsanoさん (@mackey0225) の読書感想文 : 『Javaの10年』でした。

 

11月15日に開催されたJJUG CCC 2025 FallでLazy Constantについてプレゼンしてきました。資料はこちら。

 

Lazy Constantはfinalフィールドを遅延初期化させるためのAPIで、Java 26ではJEP 526で提案されています。

 

JEP 526: Lazy Constants (Second Preview)

https://openjdk.org/jeps/526

 

LazyConstantインタフェース自体は単機能で使い方も簡単なのですが、その導入背景は理解しておいた方がよいと思います。

 

なぜLazy Constant?

Javaでも宣言的なプログラミングスタイルが増えてきたり、並列処理が当たり前に使われるようになってきて、イミュータブル性の重要度が増しています。

また、アーキテクチャー的にもDDDの導入で値クラスを使うことが多くなっています。もちろん、値クラスはイミュータブルです。

 

そこで、イミュータブルなクラスを作ることを考えるわけですが、イミュータブルなクラスの条件の1つにフィールドはすべてfinalにするということがあります。

ここで困るのが、フィールドによっては初期化に時間がかかるものがあることです。たとえば、通信やファイル読み込みなどの外部リソースにアクセスする場合などがこれに相当します。

時間のかかるフィールドの初期化がアプリケーションの起動時にまとまって発生してしまうと、ただでさえいろいろやらなければならない起動時ですが、このようなフィールの初期化のためにさらに起動時間がかかるということになってしまいます。

通常のフィールドであれば、実際にフィールドを使用する時まで初期化を遅らせることができます。しかし、finalフィールドはオブジェクトの生成時にしか初期化することができません。

そこで登場するのがfinalフィールドの遅延初期化をサポートするLazy Constantです。

 

Lazy Constantとは

Lazy Constantは値を1つだけ保持するコンテナのようなものです。

そして、保持する値の初期化は実際に使用する時まで遅延させます。もちろん、スレッドセーフなので並列処理でも使えます。

値の初期化を行うにはSupplierインタフェースを使用します。ようするに、引数なし、戻り値ありのラムダ式ですね。

重要なことが、JVMの最適化を享受しやすい実装になっていることです。LazyConstantオブジェクトから値を取得するときにはgetメソッドを使用する必要があり、しかもgetメソッド内では値が初期化されたかどうかをチェックする必要があります。これらのオーバーヘッドがあるにも関わらず、インライン化などの最適化を行いやすくなっており、最適化後は直接変数にアクセスするのとパフォーマンスが変わらなくなります。

では、このような特徴を持つLazyConstantを使ってみましょう。

 

LazyConstantの使い方

LazyConstantはインタフェースで、パッケージはjava.langです。ですから、import文は必要ありません。

メソッドは4つだけ。しかも、ほぼofメソッドとgetメソッドしか使いません。

  • static LazyConstant<T> of(Supplier<T> computingFunction) : LazyConstantオブジェクトのファクトリーメソッド
  • T get() : 値を取得するためのメソッド

 

ofメソッドでLazyConstantオブジェクトを生成して、getメソッドで値を取得するというだけです。

たとえば、Consumerクラスで遅延初期化させたいHeavyクラスのオブジェクがあるとしましょう。このオブジェクトをLazyConstantインタフェースを使用して遅延初期化させてみます。

LazyConstantインタフェースのフィールドをheavyConstantとしてfinalで定義します。

そして、実際にheavyConstantフィールドが保持する値を使用するのはconsumeメソッドだとします。

public class Consumer {
    private final LazyConstant<Heavy> heavyConstant
            = LazyConstant.of(() -> new Heavy());

    public void consume() {
        // 初回のgetメソッドコール時に
        // ofメソッドの引数で指定したラムダ式を実行し値を初期化
        // 次からは初期化後の値を取得
        var h = heavy.get();
        IO.println(h);

        // heavyを使用した処理...
    }
}

 

ofメソッドの引数のラムダ式では、Heavyオブジェクトの生成を行っています。オブジェクトの生成だけなのであれば、コンストラクター参照を使用してHeavy::newでも大丈夫です。

そして、heavyフィールドが保持している値を取得するためにgetメソッドを使用します。getメソッドの初回コール時にofメソッドの引数で指定されたラムダ式が実行されます。2回目以降は初期化された値が返ります。

 

では、このConsumerクラスを使ってみましょう。

void main() {
    var consumer = new Consumer();

    consumer.consume();
    consumer.consume();
    consumer.consume();
}

 

Heavyクラスのコンストラクターでは初期化しているというメッセージを標準出力に表示しています。

LazyConstantインタフェースはJava 26ではPreview APIなので、コンパイルや実行には--enable-previewが必要です。

>  java --enable-preview .\Main.java
ノート: C:\test\SomeData.javaはJava SE 26のプレビュー機能を使用します。
ノート: 詳細は、-Xlint:previewオプションを指定して再コンパイルしてください。
Heavy Initializing
Heavy@2002fc1d
Heavy@2002fc1d
Heavy@2002fc1d
> 

 

コンストラクターがコールされているのは1度だけで、それ以降はgetメソッドで返る値は同じオブジェクトになっていることがわかります。

 

初期化にデータが必要な場合

遅延させたい値の初期化に何らかのデータが必要になる場合はどうでしょう。

初期化にSupplierインタフェースを使うので、引数で渡すことはできません。しかし、final変数であればラムダ式の外側の変数も参照できるので次のように書くこともできます。

    public Consumer(final String filename) {
        heavyConstant = LazyConstant.of(() ->  new Heavy(filename));
    }

 

Heavyクラスのコンストラクターでは引数の文字列を表示させるようにしてみました。また、mainメソッドでも文字列を渡しています。

void main() {
    var consumer = new Consumer("Main.java");

    consumer.consume();
    consumer.consume();
    consumer.consume();
}

 

では、実行してみましょう。

>  java --enable-preview .\Main.java
ノート: C:\test\SomeData.javaはJava SE 26のプレビュー機能を使用します。
ノート: 詳細は、-Xlint:previewオプションを指定して再コンパイルしてください。
Heavy Initializing: Main.java
Heavy@2002fc1d
Heavy@2002fc1d
Heavy@2002fc1d
> 

 

このようにラムダ式を書いた時点でのfinal変数であるfilename変数をキャプチャーし、ラムダ式の実行時に使用していることが分かります。

 

初期化時の例外

外部リソースにアクセスするから遅延初期化をしたいということはけっこうあると思います。この時に困るのが例外です。

外部リソースへのアクセスはどうしても例外が発生しがちです。こんな時にどうすればよいのか?

考えられるのは以下の3つの選択肢ではないでしょうか。これについては特にJEPで触れられていないので、さくらばだったらこうするという方法です。

 

  • ラムダ式でnullを返す
  • 例外をRuntimeException例外にくるんで、スローする
  • ラムダ式の返り値をEitherもしくはResultなどにする

 

1つ目の方法は初期化のためのラムダ式で例外が発生した時に、nullを返すというものです。ラムダ式でnullを返すと、getメソッドの内部でNullPointerException例外がスローされます。

このNullPointerException例外をキャッチするという方法です。

この方法は簡単でよいのですが、残念ながら原因となる例外が隠されてしまい、スタックトレースなども引き継ぐことができません。このため、NullPointerException例外が発生したとしても、原因を調べるのが難しくなってしまいます。

 

2つ目のRuntimeException例外を使う方法は、初期化のためのラムダ式のSupplierインタフェースが検査例外をスローできないからです。これはjava.util.functionパッケージの他のインタフェースも同じですね。

そこで、何らかの例外が発生したらRuntimeException例外のcauseにセットして、RuntimeException例外をスローするという方法です。

この方法はスローするRuntimeException例外をドキュメント化しておかないと例外処理を忘れがちという問題があります。

少なくとも、マルチスレッドで使用している場合のUncaughtExceptionにならないようにしなくてはいけません。

この点に注意すれば、まぁ使える方法ではないかなと思います。

 

最後のEitherもしくはResultを使う方法は、例外を値として保存しておいて、ラムダ式の返り値として返すという方法です。

たとえば、Eitherであればleftとrightの2つの値を保持できるようになっており、正常に初期化できた時はleftに保持させ、例外が発生した時にはrightに保持させます。そして、このEitherオブジェクトをラムダ式の返り値として返します。

このため、LazyConstantインタフェースのgetメソッドの戻り値はEitherになり、leftとrightのそれぞれの場合の処理を記述します。

しかし、これだとLazyConstantというコンテナにEitherというコンテナを保持させ、実際に使うにはgetしてgetしなければならないというちょっとめんどくさいことになってしまいます。

こう考えると、finalフィールドのためにEitherを使うのはちょっとやりすぎではないかなと、さくらばは思うわけです。

 

ということで、さくらば的には2つ目のRuntimeException例外を使うかなぁ...

 

コレクションの遅延初期化

LazyConstantインタフェースではコレクションを初期化するのはちょっと難しいのですが、ListインタフェースとMapインタフェースに遅延初期化するためのメソッドが追加されています。

  • static List<E> List.ofLazy(int size, IntFunction<? extends E> computingFunction)
  • static Map<K, V> Map.ofLazy(Set<? extends K> keys, Function<? super K, ? extends V> computingFunction)

 

たとえば、0から9までを保持するリストを遅延初期化させるのであれば、次のように記述します。

private final List<Integer> nums = List.ofLazy(10, i -> i);

 

ofLazyメソッドを使用して生成したコレクションは、実際にそのコレクションにアクセスした時にofLazyメソッドの引数で指定されたラムダ式を実行して初期化を行います。

 

その他のメソッド

使うことはほぼないとは思いますが、LazyConstantインタフェースの残りの2つのメソッドについても簡単に触れておきましょう。

 

boolean isInitialized()

メソッド名で分かると思いますが、保持している値が初期化が行われたかどうか、つまりgetメソッドが1度でもコールされたかどうかを判定するためのメソッドです。初期化されて入ればtrue、初期化されていなければfalseが返ります。

 

T orElse(T other)

保持している値が初期化されていればその値を返し、初期化されていなければ引数のotherを返します。

注意しなければいけないのは、orElseメソッドの内部ではgetメソッドはコールされません。つまり、値が初期化されていなくても、初期化することはありません。

くり返しますが、初期化されていない場合はotherを返します。

 

まとめ

LazyConstantインタフェースはfinalフィールドを遅延初期化させたい場合に使用するインタフェースです。

それほど使うインタフェースではないとは思いますが、ロガーをfinalフィールドで定義したい場合などに使えます。

遅延初期化の機能だけに注目すれば、finalでないフィールドに対しても使うことが可能です。

とはいうものの、遅延初期化はそれなりにオーバーヘッドがあるので、遅延初期化を必要としないフィールドに対して使うのはやりすぎです。

遅延処理を必要とする場合に使うようにしましょう。

 

使い方だけで、意外に長くなってしまったので、LazyConstantインタフェースでの遅延処理の実装について次回説明することにします。

2025/10/11

JEPで語るJava 25 その2

このエントリーをはてなブックマークに追加

前回は、Java 25のJEPのうち初心者向け機能と小さいオブジェクトを扱う機能を紹介しました。今回はその続きです。

まずはProject Leyden関連のJEPです。

 

Project Leyden

Project Leydenは、Javaの起動時間を短縮することを目標にしたプロジェクトです。

さくらばは、てっきりGraalのネイティブイメージを取り込むだけなのかと思っていたのですが、そうではなく、様々な手段を用いて起動時間とピークパフォーマンスに達するまでの時間を短縮するための機能を提供するようです。

Java 24ではJEP 483が導入されましたが、Java 25はこの続きになるJEPが2つ導入されました。

 

JEP 514: Ahead-of-Time Command-Line Ergonomics

JEP 483はクラスロード、クラスの解析、クラス間のリンク、static初期化を行った後のイメージをキャッシュとして保存して、起動時にそれを読み込むことによって起動時間を短縮させます。

このキャッシュ(AOTキャッシュと呼ばれます)を作るには、トレーニング実行を行って、その後キャッシュ作成するという2段階が必要でした。

しかし、起動時オプションを変えて、2回実行するというのはちょっとめんどう...

ということで、JEP 514は1度の実行でトレーニング実行とAOTキャッシュ作成を行ってしまいましょうというものです。

AOTキャッシュを作成するには以下のように実行します。

 

  • AOTキャッシュ作成
    $ java -XX:AOTCacheOutput=app.aot -cp app.jar com.example.App ...

AOTキャッシュを使って実行するのは、JEP 483と同じです。

  • 本番実行
    $ java -XX:AOTCache=app.aot -cp app.jar com.example.App ...

 

もちろん、JEP 483の2段階でAOTキャッシュを作成することも可能です。

 

JEP 515: Ahead-of-Time Method Profiling

HotSpot VMはアプリケーションを実行しながら、その片手間にプロファイリングもやっています。プロファイルイングの結果から、実行中にメソッドをネイティブコードにコンパイルするなどの最適化を行っています。

逆にいうと、ピークパフォーマンスに達するまでに時間がかかるということです。

Leydenで起動時間を短縮するのが必要なユースケースでは、なおさらピークパフォーマンスに達する時間が問題になりそうです。

そこで、JEP 515では、AOTキャッシュを作成するトレーニング実行中に作成したプロファイリングの解析結果をAOTキャッシュに含めるようになりました。

AOTキャッシュを使って実行すると、プロファイリングを行わずにすぐにネイティブコンパイルなどの最適化を行うことができるので、ピークパフォーマンスに到達する時間を大幅に短縮することができます。

 

Project Leydenでは、この後、JEP 516: Ahead-of-Time Object Caching with Any GCやまだドラフトですがJEP draft: Ahead-of-Time Code Compilationなどが控えています。

 

これらのProject Leydenの機能に関して、9月のJJUGナイトセミナーでOracleのじゅくちょー阪田さんが解説してくれたのですが、その資料がまだ公開されていない...

もし、LeydenのAOTキャッシュに興味があるのであれば、資料が公開されるのを待ちましょう!

 

安全性、セキュリティ

JEP 510の鍵導出関数はとりあえずおいておいて、サポートできなくなったポーティングは削除しましょうというのがJEP 503です。

 

JEP 503: Remove the 32-bit x86 Port

32bit版のWindowsのサポートが今月終了するということで、Java 24で32bit版のWindowsのポーティングが削除されました。また、x86のポーティングも削除予定になっていました。

そして、Java 25でx86のポーティングが削除されました。

サポートされないOSに対しては安全性の問題もあるので、削除はしかたないですね。

 

モニタリング

OpenJDKでのモニタリングとプロファイリングといえば、JDK Flight Recorder (JFR)ですね。

Java 25では、このJFR関連のJEPが3つあります。1つはExperimentalなので後述することにして、残りの2つについて簡単に紹介します。

とはいうものの、1つは内部実装の話なので、機能という感じではないですけど。

 

JEP 518: JFR Cooperative Sampling

非同期にスレッドのスタックトレースをサンプリングする部分の実装を見直して、安定性を高めたというJEP。

使い方の変更はないので、Java 25のJFRであれば安定してスタックトレースを取得できるようになるはずです。

 

JEP 520: JFR Method Timing & Tracing

メソッドがコールされたタイミングや回数などをサンプリングではなく、実測で調べるためのJFRのイベントが追加されました。

追加されたイベントはjdk.MethodTimeingとjdk.MethodTraceの2種類です。

これらのイベントを使用することで、メソッドがコールされたタイミングとメソッドの処理時間、そしてスタックトレースを取得することができます。

 

その他

Standard JEPの残った2つは、Project Loom関連のScoped Valueと、GCのShenandoah GCに関連したJEPです。

 

JEP 506: Scoped Values

ThreadLocalクラスはミュータブルでいろいろと問題のあるクラスですが、それをある程度置き換えることができるのがScopedValueクラスです。

すべてではないのはScopedValueクラスがイミュータブルなクラスなので、ThreadLocalクラスでsetメソッドを使っているようなケースは置き換えられないからです。

 

使い方は簡単で、ScopedValueインスタンスをnewInstance()メソッドで作成しておいて、そこにstaticメソッドのwhere()メソッドで保持する値をバインドさせます。

where()メソッドの返り値の型はScopedValue.Carrierクラスです。このScopedValue.Carrierクラスのrun()メソッド、もしくはcall()メソッドで実行するタスク内だけでScopedValueオブジェクトにバインドしたデータを使用できます。

つまり、run()メソッドとcall()メソッドで指定する関数だけがスコープになるわけです。

 

たとえば、時間を共有したい場合を考えてみます。

// スコープで共有するScopedValueオブジェクト
final ScopedValue<LocalDateTime> TIME = ScopedValue.newInstance();

void task1() {
  // get()メソッドで共有したデータを取得
  System.out.println("Task1: " + TIME.get());
}

void task2() {
  // get()メソッドで共有したデータを取得
  System.out.println("Task2: " + TIME.get());
}

void main() {
  Runnable r = () -> {
    // whereメソッドでTIMEに現在時刻をバインド
    // runメソッドで指定するラムダ式の中だけでバインドしたデータを共有
    ScopedValue.where(TIME, LocalDateTime.now())
               .run(() -> {
                  task1();
                  task2();
                });

  };

  try (var executor = Executors.newCachedThreadPool()) {
    executor.submit(r);
  }
}

 

とはいうものの、ThreadLocalクラスやScopedValueクラスを使ったデータの共有はできるだけ避ける方がよいです。共有したデータのせいでスケールしないことも多いですし、バグも発生しやすいです。

ScopedValueクラスを安易に使うよりは、もう一度設計を見直すことの方が賢明だと思います。

たとえば、上のコードでもtask1()メソッドとtask2()メソッドの引数として時間を渡した方が分かりやすいですよね。共有せずに済むのであれば、共有しない設計にすべきです。

 

JEP 521: Generational Shenandoah

Red Hatが進めていたShenandoah GCは、以前のZGCのように世代別GCではなかったのですが、ZGCの後を追うように世代別GCになりました。

Shenandoahで世代別モードにするには、まず実行時オプションの-XX:+UseShenandoahGCでGCにShenandoahを指定します。そして、同じく実行時オプションで-XX:ShenandoahGCMode=generationalを指定します。

 

Preview

ここからはお試し機能。まずは、Preview JEPからです。

Preview JEPは4種類。JEP 507はJava 25でStandardになると予想していたんですけど、Previewでしたね。残念。

 

JEP 470: PEM Encodings of Cryptographic Objects (Preview)

1つ目のPreview JEPは鍵導出関数に関するAPIです。

 

JEP 502: Stable Values (Preview)

StableValueクラスはfinalなフィールドに対して遅延初期化を行うためのクラスです。

それにしても、java.langパッケージにScopedValueクラスとStableValueクラスという似たようなクラスができると混乱するよなぁ... と思っていたら、Java 26がターゲットのJEP 526ではLazyConstantクラスに名前が変わりました!

LazyConstantクラスに関してはJJUG CCC 2025 Fallでプレゼンすることになったので、その時に詳しく解説します。

 

JEP 505: Structured Concurrency (Fifth Preview)

Structured ConcurrencyはなかなかStandard JEPになりませんが、複数の非同期実行のタスクの結果をまとめることなどに使用するAPIです。

Java 25で5回目のPreviewですが、APIに変更があったためJava 26でもう一度Previewということになっています(JEP 525)。

Java 24まではStructuredTaskScopeクラスのインスタンシエーションにはnewを使用していたのですが、Java 25でファクトリーメソッドのopen()メソッドが導入されています。

また、全部成功したらとか、1つでも失敗したらなどの条件を表すために、Java 24まではStructuredTaskScopeクラスのサブクラスで表していましたが、JEP 505ではStructuredTaskScope.Joinerインタフェースに委譲するようになりました。この方が分かりやすいですし、今後条件の拡張しやすくていいですね。

 

JEP 507:Primitive Types in Patterns, instanceof, and switch (Third Preview)

パターンマッチングにプリミティブ型を使用できるようにするのがJEP 507です。

JEP 507は変更なしだったのですが、まだドラフトなのですが次のJEPで変更が入ったので、Standardになるのは当分先になってしまいました。残念。

 

Experimental

Experimental JEPは1つだけで、JFRの機能追加に関してです。

 

JEP 509: JFR CPU-Time Profiling (Experimental)

CPUの計測を行うJFRのイベントが追加されました。ただし、Linuxだけです。

イベントはjdk.CPUTimeSampleです。

今のところ、新しいJEPは提案されていないようなので、Java 26でStandard JEPになるかもしれません。

 

Incubator

最後のIncubator JEPは、Value Classが導入するまでずっとIncubatorのままのあれです。

 

JEP 508: Vector API (Tenth Incubator)

ベクトル計算を行うためのVector APIですが、JEP 508ではいくつかの変更がありました。ただ、Value Classが導入されるまでIncubatorのままなので、今それを調べてもなぁ... と調べるモチベーションが上がりません😰

 

まとめ

Java 25はLTSということもあり、Standard JEPが多いリリースでした。

特にJEP 512のメインクラスの省略は意外に便利です。また、JEP 506のScoped Valueも正式化したので、今後ThreadLocalの置き換えが進むかもしれません。

 

ところで、Java 25とは関係ないのですが、ずっとドラフトのままだったValue Classがサブミットされて、通常のJEPにやっとなりました。JEP 401: Value Classes and Objectsです。

まだ、ターゲットバージョンが決まっていないのですが、早ければJava 26からPreviewが始まるかもしれません。

いつ導入されるのか全然わからなかったValue Classですが、やっと一歩踏み出した感じですね。