導入:なぜCompletableFutureが必要なのか
現代のJava開発において、Web APIの呼び出しや重いファイル操作をメインスレッドで行うと、アプリケーション全体のレスポンスが低下してしまいます。CompletableFutureは、これらの処理を別スレッドで実行し、完了を待たずに次の処理へ進むための強力なツールです。非同期処理を適切に扱うことで、システムのスループットを劇的に向上させることができます。
基礎知識:非同期処理の基本用語
非同期処理を理解するために、以下の2つのメソッドの役割を整理しましょう。
supplyAsync: 戻り値がある処理を実行する場合に使用します。結果を後で受け取りたい場合に最適です。
runAsync: 戻り値が必要ない処理(ログ出力や通知など)を非同期で実行する場合に使用します。
これらはデフォルトでForkJoinPool.commonPool()を使用しますが、負荷の高い処理を行う場合は、後述するスレッドプールを明示的に指定するのが現場の鉄則です。
実装:CompletableFutureの基本操作
非同期処理を実装する際は、処理の終了後に「どう繋げるか」を意識します。
・thenApply: 処理結果を受け取り、変換して次の処理へ渡す。
・thenAccept: 処理結果を受け取り、消費する(戻り値なし)。
・exceptionally: 処理中に例外が発生した場合のフォールバック処理を記述する。
サンプルプログラム:非同期処理の実行例
以下のコードは、外部API取得を想定した非同期処理のサンプルです。そのままコピーして動作を確認できます。
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class AsyncDemo {
public static void main(String[] args) {
// 現場ではスレッドプールを個別に定義するのがベストプラクティスです
ExecutorService executor = Executors.newFixedThreadPool(2);
// 戻り値がある非同期処理 (supplyAsync)
CompletableFuture
// 重い処理をシミュレート
try { Thread.sleep(1000); } catch (InterruptedException e) {}
return "データ取得完了";
}, executor);
// 処理が終わった後のアクションを設定
future.thenAccept(result -> System.out.println("結果: " + result))
.exceptionally(ex -> {
System.err.println("エラー発生: " + ex.getMessage());
return null;
});
// 戻り値がない非同期処理 (runAsync)
CompletableFuture.runAsync(() -> {
System.out.println("バックグラウンドでログを保存中...");
}, executor);
// メインスレッドが終わらないように待機(実務ではjoinやgetを使用)
future.join();
executor.shutdown();
}
}
応用と注意点:現場での落とし穴
1. スレッドプールを指定する: デフォルトの共通プールは他の箇所からも共有されるため、長時間ブロッキングされるタスクを投げるとアプリ全体が停止する危険があります。必ず専用のExecutorServiceを渡しましょう。
2. 例外処理を忘れない: 非同期処理内で発生した例外は、適切にハンドリングしないと隠蔽されてしまい、デバッグが困難になります。必ずexceptionallyを使用してください。
3. Java 21以降の選択肢: 現在はVirtual Threads(仮想スレッド)が登場しています。単純な非同期処理であれば、CompletableFutureで複雑にチェーンするよりも、仮想スレッドを使って同期的に書く方がコードの可読性が高い場合もあります。要件に応じて使い分けるのがシニアエンジニアの視点です。

コメント