X(Twitter) Zenn GitHub RSS 共有

ScopedValue

作成日時:2025-09-23
更新日時:2025-09-23

ScopedValueのメモ。

JDK 21で正式に導入されたAPI。
ScopedValue (Java SE 21 & JDK 21)

従来のThreadLocalと似ているが、決定的に違う点がいくつかある。

ScopedValue:コンテキスト情報を親から子へ伝播させたい場合。
ThreadLocal:スレッドごとに独立した状態を維持したい場合。

ScopedValue<SomeObject>ならば、ScopedValueそのものはイミュータブルだが、SomeObjectの中は変更可能

import java.util.concurrent.ScopedValue;

public class ScopedValueExample {
    // ScopedValueのインスタンスを作成
    private static final ScopedValue<String> USER_ID = ScopedValue.newInstance();

    public static void main(String[] args) {
        String userId = "user123";

        // Scoped Valueのスコープ内で処理を実行
        ScopedValue.where(USER_ID, userId)
                .run(() -> {
                    // ここがスコープ内
                    System.out.println("Scoped Value内のユーザーID: " + USER_ID.get());
                    processRequest();
                }); // ブロックを抜けると、値は自動的に消滅する

        // スコープを抜けると値は利用できない
        try {
            System.out.println("スコープ外のユーザーID: " + USER_ID.get());
        } catch (Exception e) {
            System.out.println("スコープ外では値にアクセスできない: " + e.getMessage());
        }
    }

    private static void processRequest() {
        // 同じスレッド内のどこからでも値にアクセスできる
        System.out.println("リクエスト処理中のユーザーID: " + USER_ID.get());
    }
}

ThreadLocal

removeを忘れると、メモリリークになりうる。

public class ThreadLocalExample {
    // ThreadLocalのインスタンスを作成
    private static final ThreadLocal<String> USER_ID = new ThreadLocal<>();

    public static void main(String[] args) {
        String userId = "user123";

        // メインスレッドに値を設定
        USER_ID.set(userId);
        System.out.println("メインスレッドのユーザーID: " + USER_ID.get());

        // 新しいスレッドを作成し、実行
        Thread childThread = new Thread(() -> {
            System.out.println("子スレッドからget() -> 初期値: " + USER_ID.get());
            
            // 子スレッドに値を設定
            USER_ID.set("childUser456");
            System.out.println("子スレッドのユーザーID: " + USER_ID.get());
        });

        childThread.start();
        
        try {
            childThread.join(); // 子スレッドの終了を待つ
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 子スレッドでの変更はメインスレッドに影響しない
        System.out.println("メインスレッドに戻ってきたユーザーID: " + USER_ID.get());
        
        // 最後に値を削除するのを忘れない
        USER_ID.remove();
        System.out.println("メインスレッドから値を削除後: " + USER_ID.get());
    }
}

感想

ThreadLocalもScopedValueも、どこでそれを参照してもそのスレッドにおいて格納された値を参照できる。

そもそも色々な個所からインスタンスを参照するのが好きではない。
ローカル変数や引数で済ませられるならば、それで済ます。
引数もオブジェクトで渡せば可読性は下がらずに済む。

フレームワークを作るレベルでなければ、使わないかな。
SpringとかのTransactionalやBeanみたいな。

ScopedValueだけを集めたクラスを作って、スレッド内で参照したいものをそこに集めるのが推奨されている。

public final class RequestContext {
    public static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
    public static final ScopedValue<String> USER_ID = ScopedValue.newInstance();
    // ... その他のコンテキスト情報
}