【Java学習|初心者向け】Java 21からの新常識!Structured Concurrencyで並行処理を劇的にシンプルにする方法

導入

皆さんはこれまで、Javaで複数の非同期処理を扱う際に、FutureやCompletableFutureを使って苦労した経験はありませんか?「エラーが発生したときに他のタスクをどう止めるか」「例外処理が複雑になりすぎてコードが読めない」といった悩みは、並行プログラミングにつきものです。Java 21で導入された「Structured Concurrency(構造化並行処理)」は、これらの課題を解決し、タスクの親子関係を明確にして安全に実行するための強力な仕組みです。今回は、その中でも特に便利なShutdownOnFailureとShutdownOnSuccessについて解説します。

基礎知識

Structured Concurrencyは、複数のタスクを一つの単位として管理する概念です。従来のExecutorServiceではタスクが個別に実行され、終了のタイミングやエラー処理の管理が開発者の負担になっていました。これに対し、StructuredTaskScopeを使うと、ブロック内で実行されたタスクはすべて「親」の管理下に入ります。

今回紹介する二つのクラスは、以下の目的で使い分けます。
ShutdownOnFailure:複数のタスクのうち、一つでも失敗したら全体を失敗とみなし、他の実行中タスクをキャンセルします。
ShutdownOnSuccess:複数のタスクのうち、どれか一つでも成功した時点で、他のタスクをキャンセルして結果を返します。

実装/解決策

実装の流れは非常にシンプルです。
1. try-with-resources文でStructuredTaskScopeのインスタンスを作成する。
2. scope.forkメソッドを使ってタスクを投げ込む。
3. scope.join()で全タスクの終了を待機し、scope.throwIfFailed()でエラーをチェックする。
これだけで、複雑なスレッド管理をJavaのランタイムに任せることができます。

サンプルプログラム

以下のコードは、複数のAPIからデータを取得するようなシチュエーションを想定したShutdownOnFailureの例です。

import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.ExecutionException;

public class StructuredConcurrencyExample {
public static void main(String[] args) throws InterruptedException, ExecutionException {
// ShutdownOnFailure: どれか一つでも失敗したら全体を停止させるスコープ
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {

// タスクをフォーク(非同期実行)
var task1 = scope.fork(() -> fetchData(“Service A”));
var task2 = scope.fork(() -> fetchData(“Service B”));

// 全タスクの終了を待機
scope.join();

// エラーがあった場合は例外を投げる
scope.throwIfFailed();

// 成功した場合のみ結果を取得
System.out.println(“結果: ” + task1.get() + “, ” + task2.get());

} catch (ExecutionException e) {
System.err.println(“タスクのいずれかが失敗しました: ” + e.getCause().getMessage());
}
}

private static String fetchData(String serviceName) throws Exception {
// 実際にはここで通信処理などを行う
if (“Service B”.equals(serviceName)) {
throw new RuntimeException(“接続エラーが発生しました”);
}
return serviceName + “のデータ”;
}
}

応用・注意点

現場で活用する際のポイントをいくつか挙げます。

Virtual Threadsとの併用
Structured ConcurrencyはVirtual Threads(軽量スレッド)と組み合わせてこそ真価を発揮します。大量のタスクをフォークしても、OSスレッドを枯渇させる心配がほとんどありません。

スコープの外には出さない
forkしたタスクの結果(Subtaskオブジェクト)を、スコープの外側に持ち出さないように注意してください。これは、スレッドの生存期間とデータの整合性を保証するためです。

ライフサイクルの意識
try-with-resourcesを抜けると、まだ実行中のタスクは自動的にキャンセルされます。この「自動的なクリーンアップ」こそが、従来のExecutorServiceに対する最大のメリットです。ぜひ積極的に活用して、安全で読みやすい並行処理コードを書いてみてください。

コメント

タイトルとURLをコピーしました