PaPoo
cover
technews
Author
technews
世界の技術ニュースをリアルタイムでキャッチし、日本語でわかりやすく発信。AI・半導体・スタートアップから規制動向まで、グローバルテックシーンの「今」をお届けします。

JavaのStructured Concurrencyがまた一歩前進。JEP 533で例外処理がもっと自然になった話

キーポイント


JavaのStructured Concurrencyが、またじわっと前進しました。InfoQが伝えているのは、​JEP 533「Structured Concurrency」​JDK 27でintegrated status に入った、というニュースです。

ここでいう Structured Concurrency は、ざっくり言うと「関連する複数の処理を、ひとつのまとまりとして扱う」ための仕組みです。
たとえば、1つのリクエストを処理するために「ユーザー情報を取りに行く」「注文一覧を取る」「プロフィールを取る」といった複数の処理を同時に走らせる場面がありますよね。そういうとき、バラバラにスレッドを管理するのではなく、​親のスコープの中で子タスクをまとめて管理するのがStructured Concurrencyです。

個人的には、これはかなり筋のいい方向だと思っています。
並行処理って、便利な反面、放っておくと「いつ終わるの?」「失敗したら誰が止めるの?」「どこで起きてる処理なの?」と一気にカオスになるんですよね。Structured Concurrencyは、そのカオスを「スコープ」という形で整理しようとしている。発想としてかなり気持ちいいです。

まず何が変わったのか

今回のJEP 533で目立つのは、​例外処理の整理 です。

前のpreviewでは、Joiner.allSuccessfulOrThrow()anySuccessfulOrThrow()awaitAllSuccessfulOrThrow() が、サブタスクの失敗時に FailedException を投げていました。
それがPreview 7では、​**ExecutionException** を投げるように変わりました。

ExecutionException は、Future.get() でもおなじみの「子タスクが失敗したときに、その失敗を包んで伝える」例外です。つまり、Structured Concurrencyの例外の流れが、既存のJavaの並行処理APIとかなり近づいたわけです。

これは地味に重要です。
新しいAPIって、どうしても「専用のクセ」が出がちなんですが、今回の変更はそのクセを減らしている。​学習コストが下がるし、既存コードとの行き来もしやすい。こういう“馴染ませ方”はうまいな、と思います。

例外処理のイメージ

記事内の例では、こんな感じです。

image_0011.jpg

try (var scope = StructuredTaskScope.open()) {
    Subtask<String> user = scope.fork(() -> findUser(userId));
    Subtask<List<Order>> o = scope.fork(() -> fetchOrders(userId));
    scope.join();
    return new Response(user.get(), o.get());
} catch (ExecutionException e) {
    switch (e.getCause()) {
        case IOException ioe -> handleIo(ioe);
        case TimeoutException te -> handleTimeout(te);
        default -> throw e;
    }
}

ポイントは、join() で例外を捕まえたあと、getCause()本当の原因を見るところです。
ExecutionException 自体は「包み紙」で、中身に IOExceptionTimeoutException が入っている、というイメージですね。

これ、既存のJava開発者にはかなり理解しやすいと思います。
一方で、以前のpreviewで FailedException を使っていた人は、JDK 27へ移行するときに catchを書き換える必要がある ので、そこは注意点です。

型安全性も強化された

次の変更は、少しだけマニアックですが大事です。

StructuredTaskScopeJoiner に、​第3の型パラメータ R_X が追加されました。
これは「join() が何の例外を投げうるか」を型として表すためのものです。

以前は Joiner<T, R> だったものが、Joiner<T, R, R_X> になる、という話です。

これを聞くと「型パラメータが増えただけ?」と思うかもしれませんが、ここにはちゃんと意味があります。
Javaの型システムに、​**“このjoinはこの種類の例外を投げる”** と明示できるようになるので、ライブラリ作者にとってはかなり誠実な設計です。

個人的には、この変更は「APIをきれいに整えている途中なんだな」という印象を受けました。
最初は多少荒削りでも、previewを重ねながら、例外の扱いを型に落とし込んでいく。これはJavaらしい慎重さでもありますし、かなり好感が持てます。

open() の新しい overload も便利そう

3つ目の変更は、使い勝手に効いてきそうです。

image_0012.jpg

StructuredTaskScopeには、設定を受け取れる新しい open() の overload が追加されました。
これにより、​デフォルトのjoin policy に対して、timeoutnamethread factory などの設定をまとめて足しやすくなりました。

記事の例では、こんな書き方が紹介されています。

try (var scope = StructuredTaskScope.open(
    cfg -> cfg.withTimeout(Duration.ofSeconds(2)).withName("checkout"))) {
    scope.fork(() -> fetchCart(userId));
    scope.fork(() -> fetchProfile(userId));
    scope.join();
}

以前は、デフォルトのfail-fastな挙動に設定を足すために、Joiner を渡したりする必要がありました。
それが少し簡潔になった、というわけです。

こういう改善って、派手ではないけれど実務ではかなり効きます。
フレームワークやAPIって、​​「できること」より「書きやすさ」​ が採用率を左右することが多いんですよね。open() のこの変更は、まさにその方向の改善だと思います。

何が変わっていないのかも大事

今回のJEP 533は、記事でもはっきり「再設計ではない」と書かれています。
つまり、APIの骨格はすでに固まりつつあって、今は主に ergonomics(使いやすさ)​typing(型の整合性)​ を詰めている段階です。

構造的な保証は、引き続き維持されています。たとえば:

ここは安心材料です。
つまり「便利になったけど、根本の安全装置はそのまま」ということですね。

どんな意味があるのか

image_0013.jpg

Structured Concurrencyの狙いは、単に「スレッドを増やす」ことではありません。
むしろ逆で、​並行処理を“雑に増やさない”ための枠組み です。

従来の並行処理は、処理ごとにスレッドを飛ばして、結果をあとから集めて、失敗したら別経路で止めて……というふうに、コードがすぐ分散しがちでした。
Structured Concurrencyは、それを「この親処理のための子タスク群です」と明示して、ライフサイクルをきれいに揃えようとします。

これがうまくはまると、

という、かなり実務的なメリットがあります。

正直、並行処理のコードは「動く」だけなら何とかなるけれど、「保守できる」状態にするのが本当に難しい。
その意味で、Structured ConcurrencyはJavaにとってかなり重要な一歩だと思います。

まとめると

JEP 533の今回の更新は、見た目には地味です。
でも中身はかなり本質的で、​例外の流れを既存のJavaの感覚に寄せつつ、型安全性を強め、設定の書き味も改善している。かなり堅実な進化です。

派手な新機能というより、​​「ちゃんと使えるAPIに育ってきた」​ というニュースですね。
こういう成熟の仕方、私はかなり好きです。新しさで驚かせるのではなく、あとで振り返ったときに「結局これが一番使いやすかった」と言われるタイプの進化だからです。


参考: JEP 533 Tightens Exception Handling in Java's Structured Concurrency for JDK 27

同じ著者の記事