JEP 543がJDK 28に統合—7度のプレビュー期間を終え正式機能へ

Java JDK 28、並行処理の新標準「Structured Concurrency」が正式版に—安定性と堅牢なエラーハンドリングを実現のイメージ画像

2026年10月、Javaの並行処理モデルを刷新するJEP 543「Structured Concurrency」がJDK 28に正式に統合されました(InfoQ)。これは、JDK 19でインキュベーターAPIとして導入されて以来、7回にわたるプレビュー期間を経ての最終化となります。特にJDK 27からAPIの変更はなかったため、JDK 27で既にこの機能を利用していた開発者の方は、再コンパイル時に--enable-previewフラグを削除するだけで移行が完了します。

Structured Concurrencyは、複数の関連するサブタスクを単一の作業単位として扱い、エラー発生時には他のサブタスクをキャンセルし、親スコープがすべての子スレッドの終了を待つことを保証します。

従来のExecutorServiceやFutureを用いたパイプラインでは、親メソッドがサブタスクの完了を待たずに戻ってしまったり、サブタスクのエラーがFuture.get()が呼び出されるまで表面化しないといった問題がありました。Structured Concurrencyはこれらの問題を解決し、並行処理の堅牢性と予測可能性を大幅に向上させることを目的としています。

StructuredTaskScope APIの核となる機能

Java JDK 28、並行処理の新標準「Structured Concurrency」が正式版に—安定性と堅牢なエラーハンドリングを実現のイメージ画像

Structured Concurrencyの中心となるのは、java.util.concurrentパッケージに追加されたStructuredTaskScope<T, R, R_X extends Throwable>インターフェースです。このインターフェースは、publicコンストラクタではなく、staticファクトリメソッドを通じてインスタンス化されるsealed interfaceとして提供されます。open()ファクトリメソッドは、すべてのサブタスクが成功するのを待つか、最初の失敗で他のサブタスクをキャンセルし、ExecutionExceptionをスローするデフォルトのスコープを返します。

Response handle() throws ExecutionException, InterruptedException {
    try (var scope = StructuredTaskScope.open()) {
        Subtask<String>  user  = scope.fork(() -> findUser());
        Subtask<Integer> order = scope.fork(() -> fetchOrder());
        scope.join(); // throws ExecutionException if either subtask fails
        return new Response(user.get(), order.get());
    }
}

上記の例のように、fork()の呼び出しごとにデフォルトで仮想スレッドが開始されます。join()は、オーナーとなるスレッドが結果を待つ単一のポイントとして機能し、try-with-resourcesブロックの終わりにあるclose()は、未完了のサブタスクをキャンセルし、それらのスレッドが終了するのを待ちます。これにより、リソースリークやデッドロックのリスクが低減され、より安全な並行処理が実現します。

細分化された完了ポリシーとカスタム例外ハンドリング

Structured Concurrencyでは、完了ポリシーをJoiner<T, R, R_X>インターフェースを通じて定義できます。組み込みのJoinerにはいくつかの種類があります。

  • Joiner.awaitAllSuccessfulOrThrow(): デフォルトポリシー。すべてのサブタスクの成功を待つか、最初の失敗で例外をスローします。
  • Joiner.allSuccessfulOrThrow(): 同種のサブタスクから直接List<T>を返します。
  • Joiner.anySuccessfulOrThrow(): 最初の成功した結果を返し、残りをキャンセルします。
  • Joiner.allUntil(Predicate): 述語が一致するとスコープをキャンセルします。

特に、カスタムポリシーを定義する際に第三の型パラメータR_Xがその真価を発揮します。これにより、join()メソッドが特定の例外型を宣言できるようになりました。

例えば、レプリカからのクォーラム読み込みを待つジョイナーは、独自のチェック済み例外QuorumExceptionをスローすることができます。これは、各サブタスクの処理結果に応じて異なるエラー処理を実装したい場合に非常に強力な機能です。

エンジニア目線で見ると:並行処理のデバッグと信頼性が向上する可能性

今回のJEP 543の正式版リリースは、開発者にとって大きな恩恵をもたらすと感じます。特に、複数の非同期処理を扱う際のデバッグの困難さやエラーハンドリングの複雑さが大幅に改善されるのではないでしょうか。

従来のFuture.get()では、バックグラウンドで動き続けるサブタスクの状態を追うのが難しく、予期せぬエラーでアプリケーションが不安定になるケースもありました。Structured Concurrencyは、これらのサブタスクを明確なライフサイクルを持つ単一の「構造化された」単位として扱うため、エラーが発生した場合でも影響範囲を限定しやすくなります。

個人的には、GCPのCloud Runで動かすようなマイクロサービスアーキテクチャや、大量のリクエストを並行処理するWeb APIのバックエンドなどでの活用が期待されます。特に、複雑なビジネスロジックを持つ複数の外部サービス呼び出しを組み合わせるようなシナリオで、コードの可読性と堅牢性を高めるのに役立つでしょう。PythonのasyncioやTypeScriptのPromise.allSettledなど、他の言語にも似た概念はありますが、Javaの強力な型システムと組み合わされることで、より安全な並行処理の実装が可能になるのではないかと考えています。

sealed interfaceと組み合わせることで、考えられるエラーケースや完了パターンを網羅的に設計できる点も好感が持てます。自分の個人プロダクトで、複数のデータソースから情報を非同期で取得する処理に組み込んでみたいです。