導入
Javaでの非同期プログラミングにおいて、CompletableFutureは非常に強力なツールです。しかし、現場では「処理の完了待ち」「外部からの強制終了」「例外の伝播」の制御が曖昧になり、予期せぬスタックやデッドロックを招くケースが散見されます。今回は、CompletableFutureの実行を制御する主要な3つのメソッドを整理し、実務で安全に使いこなすための勘所を解説します。
基礎知識
CompletableFutureは、将来的に完了する計算を表すオブジェクトです。
・join(): 非同期処理の結果が出るまで現在のスレッドをブロックし、結果を返します。get()と似ていますが、チェック例外を投げないという特徴があります。
・complete(T value): まだ完了していないFutureに対し、明示的に結果を与えて完了させます。外部からのイベント駆動で値を注入する際によく使われます。
・completeExceptionally(Throwable ex): 非同期処理を失敗として終了させます。このメソッドを呼ぶと、そのFutureを待機しているスレッドのjoin()やget()で例外がスローされます。
実装/解決策
実務では、非同期タスクが正常系で終わる場合だけでなく、タイムアウトや外部入力による中断を考慮する必要があります。特に、CompletableFutureを「待ち受け用」として定義し、別のスレッドから結果を注入するパターンが重要です。
サンプルプログラム
以下のコードは、外部からの信号で非同期処理を完了させる、あるいは異常終了させる典型的なパターンです。
<コード例>
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
public class AsyncHandler {
public static void main(String[] args) {
CompletableFuture
// 別スレッドで監視タスクを起動
new Thread(() -> {
try {
// 模擬的な処理待ち
Thread.sleep(1000);
// 条件に応じて完了させる
future.complete(“成功: データ取得完了”);
} catch (Exception e) {
// 処理中に例外が発生したことを通知
future.completeExceptionally(new RuntimeException(“失敗: 処理中にエラー発生”));
}
}).start();
try {
// join()で結果を待機(完了するまでブロックされる)
String result = future.join();
System.out.println(result);
} catch (Exception e) {
// completeExceptionallyが呼ばれた場合、ここでキャッチ可能
System.err.println(“エラーハンドリング: ” + e.getMessage());
}
}
}
応用・注意点
現場で陥りやすい罠と対策を3点挙げます。
1. join()によるブロッキングの回避:
join()はブロックするため、Virtual Threads環境下ではリソース効率が悪化する可能性があります。可能な限り、thenApply()やthenAccept()などのコールバックチェーンを用いて、ブロックせずに後続処理をつなげる設計を優先してください。
2. 二重完了の防止:
complete()を一度呼んだ後に再度complete()を呼んでも、状態は変わりません(最初の一回のみ有効)。しかし、意図せず複数回呼ばれる設計はバグの温床です。AtomicBooleanなどを用いて、完了処理が一度しか通らないようにガードを入れるのが安全です。
3. 例外のラップ:
join()で発生する例外はCompletionExceptionでラップされます。例外処理を行う際は、getCause()を呼び出して本来のExceptionを取り出す必要がある点に注意してください。
今後はStructured Concurrencyの導入により、CompletableFutureの複雑な管理から解放される場面も増えますが、既存のレガシーコードやExecutorServiceベースのシステムでは、これらのメソッドを正確に制御することが堅牢なアプリケーションへの第一歩となります。

コメント