SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

DeveloperZine(デベロッパージン)- エンジニアの意思決定を支える技術情報メディア ProductZine

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

【最新Javaアップデート解説】変更点と過去バージョンからのおさらい

【Java 25リリース】シンプルになったコーディングや、堅牢になった並列処理を紹介

【最新Javaアップデート解説】変更点と過去バージョンからのおさらい 第7回

並列処理をより安全に

 Project Loomは、並列処理を改善していくためのプロジェクトで今回は2つの機能がリリースされました。これらの新機能を理解するために、まず従来のコードが抱えていた問題点から見ていきましょう。

スレッド内で変数管理をする際の既存の問題点

 スレッドごとに独立したデータを管理する場合、ThreadLocalを利用できました。しかしこのThreadLocalには、見落としがちな落とし穴がありました。

 リスト4は、ThreadLocalを使ってスレッド内で変数を管理しようとする例です。

[リスト4]ThreadLocalクラスを用いた現在の課題点(src/java/jp/enbind/java25/jep506/ThreadLocalSample.javaの抜粋)
// (省略)
// (1) スレッドで管理する変数
private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
// (省略)
private static void threadLocalSample() throws Exception{
    //  (2) 異なるスレッドで実行する
    ExecutorService service = Executors.newFixedThreadPool(2);
    //  (3) 同じスレッドを使い回す
    //ExecutorService service = Executors.newSingleThreadExecutor();

    //  (4) スレッドで処理をする
    service.submit(() ->{
        CONTEXT.set("sample1");
        IO.println(Thread.currentThread().toString() + "(1)->" + CONTEXT.get());

        Thread child = new Thread(() -> {
            //  (5) ここでは値は見えない
            IO.println(Thread.currentThread().toString() + "(1-1)->" + CONTEXT.get());
        });
        // 省略

        //CONTEXT.remove(); // (6) 削除し忘れる
    });

    Thread.sleep(10); // スレッドの実行順を分かりやすいように遅延させる

    //  (7) スレッドで処理をする
    service.submit(() -> {
        //CONTEXT.set("sample2"); // (8) 設定を忘れた場合はどうなる? ( スレッド次第 )
        IO.println(Thread.currentThread().toString() + "(2)->" + CONTEXT.get());
    });

    // (9) 本来はアクセスさせたくないスコープ
    IO.println(Thread.currentThread().toString() + "(3)->" + CONTEXT.get());
    service.shutdown();
}

 (1)ではThreadLocalインスタンスを、(2)(3)では、スレッドプールをそれぞれ準備します。詳しくは後述しますが、(2)(3)でのスレッドプールの違いによって、このコードは異なった結果となります(結果に違いが出るように、意図的に2つの問題を含ませており、その点についても後述します)。この(2)と(3)で実際にどのような違いが生じるのか、実行結果から見比べてください。

 まずは(2)を有効にした場合の結果からです。

[リスト5]newFixedThreadPool(異なるスレッド)を使った場合の実行結果
Thread[#25,pool-1-thread-1,5,main](1)->sample1
Thread[#26,Thread-0,5,main](1-1)->null
Thread[#3,main,5,main](3)->null
Thread[#27,pool-1-thread-2,5,main](2)->null

 一方、(3)を有効にした場合の結果です。

[リスト6]newSingleThreadExecutor(同じスレッド)を使った場合の実行結果
Thread[#25,pool-1-thread-1,5,main](1)->sample1
Thread[#26,Thread-0,5,main](1-1)->null
Thread[#3,main,5,main](3)->null
Thread[#25,pool-1-thread-1,5,main](2)->sample1	(結果が変化する!)

 なぜこういった違いが生まれたかと言うと、ThreadLocalがもともとスレッド間で異なる変数を保持する仕組みだからです。(2)では、別々のスレッドインスタンスで処理が実行されますが、(3)では同じスレッドが使い回されています。結果、(3)では意図せず値が共有されてしまっているのです。

 この問題には、以下の2つの技術的な課題が関わっています。

  • 値の削除漏れ(6):ThreadLocalは、使用後にremove()メソッドで値を削除することが推奨されます。これを忘れると、特にスレッドプールのようにスレッドが再利用される環境で、前の処理で設定された値が予期せず残ってしまい、バグの原因になります。
  • 値の設定漏れ(7):スレッドで値を設定し忘れると、nullが返されます。この場合、値が設定されていないのか、意図的にnullなのかが不明瞭になり、デバッグが困難になります。

 さらに子スレッドを作成して実行する場合、(5)の扱いや、スレッド外で実行されるケース(9)など、このコードだけを見ても、どのような結果になるのかがわかりにくい部分もあります。

 現在のThreadLocalはスレッドの種類によって開発者が意図しない挙動を引き起こす可能性があり、特に仮想スレッドのような新しい機能と組み合わせる場合、問題がさらに複雑になります。

 この問題を解決し、より安全な並列処理を実現するためにJEP505とJEP506が導入されました。

JEP506:スコープ内のスレッドのおける変数管理の改善

 ThreadLocalが抱える課題を解決するために、JEP506で導入された新しいクラスがScopedValueです。このクラスは、スレッド内で安全に変数を共有するための仕組みを提供します。

[リスト7]ScopedValueの利用例(src/java/jp/enbind/java25/jep506/Sample.javaの抜粋)
// (省略)
// (1) ScopedValueのインスタンスを作成
private static final ScopedValue<String> CONTEXT = ScopedValue.newInstance();

ExecutorService service = Executors.newSingleThreadExecutor();
// スレッド処理を実行
service.submit(() -> {
// (2) 値を設定して、実行
    ScopedValue.where(CONTEXT,"sample1").run(() -> {
        IO.println(Thread.currentThread().toString() + "(1)->" + CONTEXT.get());
    });
});

try {
    // (3) アクセスしてはいけないはずのスコープ
    if(CONTEXT.isBound()) {
        // ここでは必ずfalseになる
        IO.println(Thread.currentThread().toString() + "->" + CONTEXT.get());
    }
}
catch (Exception e){
    // isBound()でチェックせずアクセスすると、java.util.NoSuchElementException: ScopedValue not bound のエラーが発生する
    e.printStackTrace();
}
service.shutdown();
// (省略)

 ThreadLocalとの最も大きな違いは、コード上の制約が厳格になった点です。

 ScopedValue(1)は(2)のようにwhere()メソッドで値を設定し、run()メソッドで処理を実行します。ThreadLocalのように直接set()するメソッドは存在しませんし、スコープもより明示的な宣言が必要になります。

 また、スコープが終了すると、自動的に値が破棄されるため、ThreadLocal.remove()のような終了処理を書き忘れる心配がありません。さらに、ThreadLocalでは、スコープ外からアクセスした場合にnullが返ってくることがありましたが、ScopedValueではisBound()メソッドで値が設定されているか安全に確認できます(3)。

 もしisBound()のチェックをせずにアクセスすると、NoSuchElementExceptionがスローされ、誤った使い方をしていることがすぐにわかります。このように、ScopedValueはThreadLocalが抱えていた問題を解決し、スレッド間でデータを安全かつ厳密に共有するための堅牢な仕組みを提供します。

JEP505:構造化された並列処理に関するAPI(プレビュー版)

 並列処理の安全性と管理を向上させるために、StructuredTaskScopeという新しいクラスが導入されました。

 このAPIは、複数のタスクを単一の作業単位として扱い、そのタスク群のライフサイクルを明確に管理します。

 ScopedValueはスレッド内で安全に変数を共有できますが、新しい子スレッドには値が引き継がれません。StructuredTaskScopeは、この課題を解決するために作られました。このクラスを使うと、親スレッドのスコープを子スレッドに安全に引き継ぎ、並列タスクをより堅牢に扱うことが可能です。

 リスト7は、StructuredTaskScopeの基本的な使い方です。

[リスト8]StructuredTaskScopeの利用例(src/java/jp/enbind/java25/jep505/Sample.javaの抜粋)
ExecutorService service = Executors.newCachedThreadPool();

service.submit(() -> {
    ScopedValue.where(CONTEXT,"sample1").run(() -> {
        IO.println(Thread.currentThread().toString() + "(1)->" + CONTEXT.get());

        try {
            //  (1) 子スレッドでもCONTEXTの値を使いたい場合
            Thread child = new Thread(() -> {
                if(CONTEXT.isBound()) {
                    // 必ずfalseになる
                }
                else{
                    IO.println(Thread.currentThread().toString() + "(2)-> NOT BOUND");
                }
            });
            child.start();
            child.join();
            }
        }
        // (省略)

        // (2) 子スレッドで処理 ( プレビュー機能のため--enable-preview をつけてください )
        try (var scope = StructuredTaskScope.open()) {
            scope.fork(() -> {
                IO.println(Thread.currentThread().toString() + "(1-3)->" + CONTEXT.get());
            });
            try {
                scope.join();
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    });
});
// (省略)

 この例では、(1)のように通常のThreadを作成した場合、CONTEXTの値は子スレッドに引き継がれません。しかし、(2)のようにStructuredTaskScopeを使ってfork()メソッドでタスクを実行すると、親スレッドのScopedValueが子タスクにも伝播されます。

 これにより、関連するタスク群のライフサイクルが単一のスコープにまとめられ、エラーハンドリングも一元化されます。現在プレビュー版ですが、今後の正式リリースに向けて、その動向に注目していくといいでしょう。

最後に

 Java21以降、プレビュー版として提供されてきた多くの新機能が、いよいよJava 25で正式リリースされました。特に、mainメソッドの簡略化やモジュールインポートの改善は、スクリプト言語に比べてJavaが面倒に感じられていた点を大きく解消します。これにより、Javaを学ぶ人や、小規模なプロジェクトでJavaを使いたい人にとって、開発がより簡単で直感的なものになるでしょう。

 また、今回のJDK25では、これまであまり取り上げられることのなかったJFR(Java Flight Recorder)関連の改善が3つ含まれています。次回は、このJFRの基本的な使い方から、今回のバージョンで追加された変更点について解説します。

参考資料

この記事は参考になりましたか?

連載通知を行うには会員登録(無料)が必要です。
既に会員の方はを行ってください。
【最新Javaアップデート解説】変更点と過去バージョンからのおさらい連載記事一覧

もっと読む

この記事の著者

WINGSプロジェクト 小林 昌弘(コバヤシ マサヒロ)

WINGSプロジェクト について> 有限会社 WINGSプロジェクト が運営する、テクニカル執筆コミュニティ(代表 ...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

山田 祥寛(ヤマダ ヨシヒロ)

静岡県榛原町生まれ。一橋大学経済学部卒業後、NECにてシステム企画業務に携わるが、2003年4月に念願かなってフリーライターに転身。Microsoft MVP for Visual Studio and Development Technologies。執筆コミュニティ「WINGSプロジェクト」代表。主な著書に「独習シリーズ(Java・C#・Python・PHP・Ruby・JSP&サーブレットなど)」「速習シリーズ(ASP.NET Core・Vue.js・React・TypeScript・ECMAScript、Laravelなど)」「改訂3版JavaScript本格入門」「これからはじめるLaravel実践入門」「はじめてのAndroidアプリ開発 Kotlin編 」他、著書多数

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

CodeZine(コードジン)
https://codezine.jp/article/detail/22231 2025/09/24 11:00

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー