本ページはプロモーションが含まれています

Java入門

Java非同期処理とは?基本と4つの主要手法を解説

Java非同期処理とは?基本と4つの主要手法を解説

トム

SESで約8年、環境を変えられないもどかしさの中にいました。32歳で自社開発企業に転職し、今はClaude Codeと自動化で、仕事も時間も組み直している途中です。
・Javaバックエンドエンジニア(Java歴12年以上)
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎
詳細なプロフィール

Javaの非同期処理、概念はわかるのに「いざ実装すると手が止まる」という方は多いです。

この記事では、Thread・ExecutorService・Future・CompletableFutureの4つの手法を、コピペで動くサンプルコード付きで解説します。

10年以上Javaで開発してきた私が、現場で実際に使い分けている判断基準もあわせて紹介します。

「同期処理との違い」「どの手法を選ぶべきか」「デッドロックや例外処理の落とし穴」まで、この1記事でJavaの非同期処理の全体像がつかめます。

Javaの非同期処理とは?【基本をわかりやすく解説】

Java非同期処理と同期処理の違いを示すイメージ図

Javaの非同期処理は、システムのパフォーマンスと応答性を向上させる上で欠かせない技術です。まずは、その基本的な考え方から見ていきましょう。

非同期処理と同期処理の違い

私たちのプログラムは通常、「同期処理」で動いています。

同期処理とは、タスクAが終わるまでタスクBを開始しない、直線的な処理方法です。

スーパーのレジで、前の人の会計が完全に終わるまで自分の会計が始まらないのと同じです。

シンプルで分かりやすい反面、時間のかかる処理(タスクA)があると、後続の処理(タスクB)がすべて待たされてしまいます。

対して非同期処理は、タスクAの完了を待たずに、先にタスクBを開始できる処理方法を指します。

レストランで料理を注文(タスクA)した後、ウェイターは料理が出来上がるのを待たずに、別のテーブルの注文を取りに行きます(タスクB)。

Javaの非同期処理では、時間のかかる処理を別の作業員(スレッド)に任せ、メインの作業員は待たずに次の仕事に進むイメージです。

これにより、システム全体としての処理能力(スループット)が向上します。

同期と非同期:全体の待ち時間の違い
同期:順番に待つ
API A
API B
集計
非同期:待ち時間を重ねる
API A
API B
集計
時間 →
AとBが互いに独立していれば、同時に走らせた分だけ全体の完了が早まる。

なぜ非同期処理が必要なのか

非同期処理の最大の目的は、ユーザー体験の向上とシステムリソースの有効活用です。

Webアプリケーションで、ボタンを押した後に重い処理が同期的に実行されると、ユーザーは処理が終わるまで画面が固まった(フリーズした)状態で待たなければなりません。これは大きなストレスです。

Java非同期処理を使えば、重い処理をバックグラウンドで実行させ、ユーザーにはすぐに「処理を受け付けました」と応答を返せます。

また、特にI/O処理(ファイルの読み書き、データベースアクセス、外部API呼び出し)では、処理がデータの到着を待っているだけの「待機時間」が多く発生します。

同期処理ではこの待機時間中、呼び出し元のスレッドが何もできずに占有されたままですが、非同期処理なら、待機している間に別のタスクを進めることが可能。

リソースを無駄なく使い切れるのです。

Javaでの非同期処理の主な仕組み

Javaにおける非同期処理は、主に「スレッド」という仕組みによって実現されます。

スレッドとは、プログラム内での「処理の流れ」の単位です。

通常のプログラムは1つの流れ(メインスレッド)で動きますが、Javaは複数のスレッドを同時に実行できる「マルチスレッド」に対応しています。

Javaの非同期処理では、メインスレッドとは別に新しいスレッドを作成し、時間のかかる処理をそちらに任せます。

Java 1.0の時代からThreadクラスが提供されていましたが、スレッドの管理が煩雑でした。

Java 5でExecutorServiceが、Java 8でCompletableFutureが登場。

さらにJava 21(2023年9月リリース、LTS)では仮想スレッド(Virtual Threads)が正式導入され、Javaの非同期・並行処理は格段に扱いやすく進化し続けています。

Javaの非同期処理で使われる主要な手法

Javaの非同期処理で使われる4つの主要手法

Javaで非同期処理を実現するには、いくつかの方法があります。ここでは代表的な4つの手法を、進化の順に解説します。

スレッド(Thread)を使った非同期処理

最も基本的かつ原始的な方法が、ThreadクラスやRunnableインタフェースを直接使うやり方です。

Runnableは「実行したい処理」を定義するインタフェースで、Threadクラスにその処理を渡してstart()メソッドを呼び出すと、新しいスレッドが起動して処理が非同期で実行されます。

Thread/Runnableを直接使う方法はJava非同期処理の土台を理解するのに役立ちます。

しかし、処理のたびにスレッドを生成・破棄するため、そのコストが無視できません。

また、スレッドの数を管理しないと、リソースを使い果たしてシステムダウンにつながる危険もあります。

ExecutorServiceによるタスク管理

スレッド管理の煩雑さを解決するために、Java 5でExecutorService(実行サービス)が導入されました。

ExecutorServiceは、「スレッドプール」という仕組みを提供します。

スレッドプールは、あらかじめ決められた数のスレッドを作成して待機させておく「スレッドの箱」のようなものです。

非同期で実行したいタスクが発生すると、ExecutorServiceがスレッドプールの空いているスレッドにタスクを割り当てます。

処理が終わったスレッドは破棄されず、プールに戻って次のタスクを待ちます。

スレッドプールを使うことで、スレッドを生成・破棄するコストを抑えられ、システム全体のスレッド数を適切に管理できます。

2026年時点のJava開発において、スレッドを直接扱うことは稀です。

ExecutorServiceまたはCompletableFutureを使うのが一般的で、Java 21以降のプロジェクトでは仮想スレッド(Virtual Threads)も有力な選択肢です。

FutureとCallableを使った結果の取得

Runnableは処理を実行するだけで、結果を返すことができませんでした。非同期処理の結果が欲しい場合もあります。

そこで使われるのがCallableインタフェースとFutureインタフェースです。

  • Callable: Runnableと似ていますが、処理結果を返すことができ、例外をスローできます。
  • Future: ExecutorServiceにCallableを渡してsubmit()すると返ってくるオブジェクト。「非同期処理の結果引換券」とイメージすると分かりやすいです。

Futureのget()メソッドを呼び出すと、非同期処理の結果を取得できます。

ただし、このget()は、処理が完了するまで呼び出し元のスレッドをブロック(待機)させてしまう点に注意が必要です。

処理が終わっていなければ、結局そこで待たされてしまいます。

CompletableFutureによるモダンな非同期処理

Futureの「get()でブロックしてしまう」という弱点を克服し、さらに柔軟な非同期処理を実現するために、Java 8でCompletableFutureが登場しました。

CompletableFutureは、Java非同期処理の集大成ともいえる機能です。

CompletableFutureを使うと、「処理Aが終わったら、その結果を使って処理Bを実行し、Bが終わったらCを実行する」といった、処理の依存関係を流れるように(メソッドチェーンで)記述できます。

また、「処理Aと処理Bを並列に実行し、両方が終わったらその結果を合体させて処理Cを実行する」といった複雑な非同期処理の合成も簡単です。

get()でブロックする代わりに、コールバック(処理が終わったら呼び出される処理)を登録するスタイルで記述するため、スレッドを無駄に待機させません。

例外処理もチェーンの中に組み込めるため、コードが読みやすくなります。

Spring Boot @Asyncで手軽に非同期化する方法

Spring Bootを使っている場合、@Asyncアノテーションを付けるだけでメソッドを非同期化できます。

内部的にはCompletableFutureと同じくスレッドプールで動作しますが、設定がはるかに簡単です。

まず、メインクラスに@EnableAsyncを付与して非同期処理を有効化します。

次に、非同期にしたいメソッドに@Asyncを付け、戻り値をCompletableFuture<T>にすれば、呼び出し元は結果を待たずに次の処理へ進めます。

スレッドプールのサイズはTaskExecutorのBean定義でカスタマイズ可能です。

デフォルトのスレッドプールはキューが無制限のため、本番環境では必ず上限を設定しましょう。

注意点として、同一クラス内でのメソッド呼び出しでは@Asyncが効かないという制約があります。

Springのプロキシ経由で呼び出す必要があるため、別クラスに切り出すか、自己参照パターンを使いましょう。

どの手法を選ぶ?4つの非同期処理方式の比較表

ここまで紹介した4つの手法(+Spring @Async)を、用途別に比較します。

迷ったら、多くの場面で第一候補になるCompletableFutureから検討するとよいでしょう。

私の経験上、チーム開発で最も多いのは「ExecutorService + CompletableFuture」の組み合わせです。

ExecutorServiceでスレッドプールを明示的に管理しつつ、タスクの合成やエラーハンドリングはCompletableFutureに任せます。

可読性と制御性のバランスが最も良いパターンです。

手法結果取得ブロッキングチェーン処理推奨ユースケースJava版
Thread / Runnable×-×学習・簡易な非同期1.0〜
ExecutorService△(Futureで)get()でブロック×スレッドプール管理5〜
Future + Callable○get()でブロック×結果が必要な単発タスク5〜
CompletableFuture○ノンブロッキング可○複数API並列・合成処理8〜
Spring @Async○ノンブロッキング可○(CF経由)Spring Boot Webアプリ-
Virtual Threads (Java 21+)○ブロッキングしても軽量(待機中は仮想スレッドが中断)○(CF併用)I/Oバウンドの大量並行処理21〜

Java非同期処理の実装例【コピペで動くサンプルコード】

Java非同期処理のサンプルコード実装例

理論だけでは分かりにくいので、Java非同期処理の具体的なコード例を見てみましょう。

スレッドを使った基本コード例

Runnableインタフェースを使い、新しいスレッドで処理を実行する最もシンプルな例です。

java
public class ThreadExample {
    public static void main(String[] args) {
        System.out.println("メインスレッド: 開始");

        // 実行したい処理をRunnable(ラムダ式)で定義
        Runnable task = () -> {
            try {
                System.out.println("  別スレッド: 開始");
                Thread.sleep(2_000); // 2秒かかる重い処理をシミュレート
                System.out.println("  別スレッド: 終了");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        // Runnableを渡してThreadを生成
        Thread subThread = new Thread(task);

        // 別スレッドを起動
        subThread.start();

        // メインスレッドは待たずに次の処理へ
        System.out.println("メインスレッド: 別の作業を実行中...");
        System.out.println("メインスレッド: 終了");
    }
}

実行すると、メインスレッドの「終了」が、別スレッドの「終了」よりも先に表示されるはずです。

ExecutorService+Futureの実装例

スレッドプールを使い、Callableで結果を取得する例です。

java
import java.util.concurrent.*;

public class ExecutorFutureExample {
    public static void main(String[] args) {
        System.out.println("メインスレッド: 開始");

        // 2つのスレッドを持つスレッドプールを作成
        ExecutorService executor = Executors.newFixedThreadPool(2);

        // 結果を返す処理をCallable(ラムダ式)で定義
        Callable<String> task = () -> {
            System.out.println("  別スレッド: 開始");
            Thread.sleep(2_000); // 2秒かかる処理
            System.out.println("  別スレッド: 終了");
            return "非同期処理の結果"; // 文字列を返す
        };

        // スレッドプールにタスクを投入し、Future(引換券)をもらう
        Future<String> future = executor.submit(task);

        System.out.println("メインスレッド: 別の作業を実行中...");

        try {
            // future.get()で結果を取得。処理が終わるまで待機(ブロック)する
            String result = future.get();
            System.out.println("メインスレッド: " + result + "を受け取りました");
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }

        // スレッドプールをシャットダウン(忘れずに)
        executor.shutdown();
        System.out.println("メインスレッド: 終了");
    }
}

future.get()で結果を受け取れますが、その時点で処理が終わっていないと待機が発生します。

CompletableFutureで並列処理を行う例

CompletableFutureを使い、2つのAPI呼び出しを並列実行し、結果を結合するモダンな例です。

java
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

public class CompletableFutureExample {

    public static void main(String[] args) {
        System.out.println("メイン処理: 開始");

        // 非同期処理1: ユーザー情報を取得(2秒かかると仮定)
        CompletableFuture<String> userFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println("  [A] ユーザー情報 取得開始...");
            sleep(2_000);
            System.out.println("  [A] ユーザー情報 取得完了");
            return "ユーザーA";
        });

        // 非同期処理2: 商品情報を取得(3秒かかると仮定)
        CompletableFuture<String> productFuture = CompletableFuture.supplyAsync(() -> {
            System.out.println("  [B] 商品情報 取得開始...");
            sleep(3_000);
            System.out.println("  [B] 商品情報 取得完了");
            return "商品B";
        });

        System.out.println("メイン処理: [A]と[B]の結果を待たずに進行中...");

        // 処理1と処理2が両方終わったら、結果を結合して次の処理を行う
        CompletableFuture<String> combinedFuture = userFuture.thenCombine(
            productFuture,
            (user, product) -> { // userが[A]の結果, productが[B]の結果
                System.out.println("  [C] 結合処理 開始");
                return user + " が " + product + " を購入";
            }
        );

        // 最終結果を受け取って表示する(コールバック)
        CompletableFuture<Void> printFuture = combinedFuture.thenAccept(result -> {
            System.out.println("最終結果: " + result);
        });

        System.out.println("メイン処理: 完了(非同期処理の完了は待たない)");

        // サンプルコードのため、非同期処理が終わるまでメインスレッドを待機
        printFuture.join(); // 最終結果の表示まで待つ。join()はget()と似ているが、検査例外をスローしない
    }

    // 擬似的に時間がかかる処理
    private static void sleep(int millis) {
        try {
            TimeUnit.MILLISECONDS.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

処理A(2秒)と処理B(3秒)が並列で動くため、get()で待つ実装と比べて、システム全体は約3秒で完了します(逐次なら2+3=5秒)。

非同期処理の結果を待つ3つの方法|get・join・コールバック

非同期処理の結果をいつ・どうやって受け取るかは、実装で迷いやすいポイントです。主に3つの方法があります。

  1. Future.get(): 結果が返るまで呼び出し元スレッドをブロックします。タイムアウトを指定でき(get(5, TimeUnit.SECONDS))、処理が長すぎる場合にTimeoutExceptionで打ち切れます。
  2. CompletableFuture.join(): get()と同様にブロックしますが、検査例外(ExecutionException)をスローしない点が異なります。try-catchを書きたくない場面で便利です。
  3. thenAccept() / thenApply()(コールバック): 結果が出たら自動で次の処理を実行するノンブロッキング方式。メインスレッドを止めたくないWebアプリで推奨されます。

判断基準はシンプルです。メインスレッドを止めてよいならjoin()、止めたくないならthenAccept()系のコールバックを使いましょう。

Javaの非同期処理を使うメリットと注意点

Java非同期処理のメリットと注意点

Java非同期処理は強力ですが、正しく使わないと予期せぬ不具合を生みます。メリットとリスクを理解しましょう。

パフォーマンス向上とスケーラビリティ

非同期処理の最大のメリットは、システムの応答性(レスポンス)と処理能力(スループット)の向上です。

特に、データベースアクセスや外部API呼び出しといった、結果の到着を待っている時間(I/O待機)が多い処理で絶大な効果を発揮します。

待機時間にスレッドを解放し、別のリクエスト処理に回せるため、少ないリソースで多くのリクエストをさばけるようになります(スケーラビリティの向上)。

また、CPUをフルに使う計算処理(CPUバウンド)であっても、マルチコアCPUの各コアに処理を割り当てる「並列処理」を行うことで、全体の処理時間を短縮可能です。

デッドロックや競合状態などのリスク

非同期処理(マルチスレッド)を導入する上で、最も注意すべきリスクが2つあります。

リスク

  1. 複数のスレッドが、同じデータ(変数やオブジェクト)に同時に読み書きしようとすると発生します。アクセスする順番によって結果が変わってしまい、意図しない値になる危険な状態です。対策としては、synchronizedキーワードで一度に1つのスレッドしかアクセスできないように制御(排他制御)したり、Atomicクラスを使ったりします。
  2. 2つ以上のスレッドが、互いに相手が保持しているリソース(ロック)の解放を待ち続け、全員が動けなくなる状態です。対策としては、ロックを取得する順序を常に一定にする、などのルール作りが重要になります。

これらの問題は「スレッドセーフティ」と呼ばれ、Java非同期処理を扱う上で最も難解な部分です。

例外処理やエラーハンドリングのコツ

非同期処理の例外は、呼び出し元のスレッドに直接伝わらないため、ハンドリングを忘れがちです。

  • Threadで例外がスローされると、そのスレッドは停止しますが、メインスレッドには通知されません。UncaughtExceptionHandlerで捕捉する必要があります。
  • Futureの場合、非同期処理中の例外はfuture.get()を呼び出したタイミングでExecutionExceptionとしてスローされます。get()をtry-catchで囲む必要があります。
  • CompletableFutureでは、exceptionally()やhandle()といった専用のメソッドチェーンを使って、エラー処理を記述するのがベストプラクティスです。

エラーハンドリングを怠ると、処理が失敗しているのにシステムは正常に動き続けているように見え、デバッグが困難になります。

実務上のTipsとして、非同期処理では必ずログにスレッド名を出力するようにしましょう。

Thread.currentThread().getName()をログに含めておくと、障害調査時にどのスレッドで例外が発生したかを追跡できます。

私はこの習慣がなかったために、複数スレッドの例外ログが混在し原因特定に苦労した経験があります。

実践的な活用シーンとベストプラクティス

Java非同期処理の実践的な活用シーン

Javaの非同期処理は、どのような場面で活躍するのでしょうか。

Webアプリ開発での非同期処理(例:API呼び出し)

現代のWebアプリケーション開発、特にSpring Bootなどを使った開発では、非同期処理が多用されます。

ユーザーからリクエストを受け付けた際、その処理に時間がかかる場合(例:複数の外部APIを呼び出して情報を集める、重いデータベースクエリを実行する)、その処理を非同期化します。

Springでは@Asyncアノテーションをメソッドに付けるだけで、簡単に処理を非同期化できます。

これにより、Webサーバーの貴重なスレッド(サーブレットスレッド)をすぐに解放できます。

スレッドがすぐに解放されれば、そのスレッドは別のユーザーからのリクエストを受け付けられるようになり、サーバー全体の応答性が劇的に改善します。

並列処理によるデータ分析の高速化

大量のデータを処理するバッチプログラムや、データ分析基盤でもJava非同期処理は活躍します。

例えば、1億件のログデータを処理する必要があるとします。これを1つのスレッドで逐次処理すると膨大な時間がかかります。

データを1,000万件ずつ10個に分割し、ExecutorServiceを使って10個のスレッドで「並列処理」させれば、理論上は処理時間を1/10に短縮できます(CPUコア数によります)。

Java 8から導入された「Parallel Streams」を使うと、このようなデータ並列処理をさらに簡単に記述できます。

なお、Java 21以降では仮想スレッド(Virtual Threads)を使った軽量な並行処理も選択肢に入ります。

I/Oバウンドな処理では仮想スレッドの方が効率的なケースが多いため、プロジェクトのJavaバージョンに応じて検討しましょう。

CompletableFutureのthenCombine/thenCompose活用術

CompletableFutureを使いこなす鍵は、メソッドの使い分けです。

  • thenCombine (結合)「API Aの結果」と「API Bの結果」が両方必要な場合に使います。AとBは並列に実行され、両方が完了した時点で、2つの結果を使って次の処理を行います。
  • thenCompose (合成)「API Aの結果」を使って、「API Bを呼び出す」必要がある場合に使います。処理Aと処理Bに依存関係がある場合です。thenApply(Aの結果を加工するだけ)と似ていますが、thenComposeは次の非同期処理を返す場合に用いることで、CompletableFutureのネスト(入れ子)を防ぎ、コードを平坦に保てます。

これらのメソッドを適切に使い分けることで、「コールバック地獄」を避け、読みやすくメンテナンス性の高い非同期コードが書けます。

Java非同期処理に関するよくある質問

FutureとCompletableFutureの違いは何ですか?

Futureは非同期処理の結果を表すが、完了を待つ手段が基本的にブロッキングなget()しかありません。CompletableFutureはthenApply/thenComposeなどのコールバックチェーンを組めるため、処理完了時に自動的に次の処理を実行でき、コールバック地獄を避けられます。

非同期処理でよくあるデッドロックの原因は何ですか?

限られたスレッドプールで、あるタスクが別のタスクの完了を待ちながらプール内のスレッドを占有し続けることが典型的な原因です。ExecutorServiceのスレッド数不足や、CompletableFuture内で同じプールを再帰的に使う設計は特に注意が必要です。

仮想スレッドが登場した今、ExecutorServiceは不要になりますか?

不要にはなりません。仮想スレッドはExecutorServiceのExecutors.newVirtualThreadPerTaskExecutor()経由で使うのが標準的な使い方で、両者は排他ではなく組み合わせて使う関係です。CPU集約的な処理では従来のスレッドプールが引き続き適しています。

まとめ|Java非同期処理の実装で押さえるべきポイント

Javaの非同期処理は、アプリケーションのパフォーマンスを飛躍的に高める可能性を秘めた技術です。

しかし、その強力さゆえに、スレッドセーフティやエラーハンドリングといった特有の難しさも伴います。

初心者がつまずきやすいポイント

多くの初学者がつまずくのは、やはり「競合状態」と「例外処理」です。

「自分のローカル環境では動いていたのに、本番環境で時々データがおかしくなる」という現象は、多くが競合状態の見落としによるものです。

もう1つ、私が多くの後輩エンジニアで見てきたパターンは「非同期にすれば速くなる」という誤解です。

CPUバウンドな処理を非同期化しても、コア数以上にスレッドを増やせばコンテキストスイッチのオーバーヘッドで逆に遅くなります。

非同期処理が効果を発揮するのは、あくまでI/O待ちが発生する処理であることを意識してください。

また、非同期処理中の例外が握りつぶされ、エラーに気づけないケースも頻発します。

CompletableFutureのexceptionally()などを使い、エラーパスを常に意識する訓練が必要です。

非同期処理を安全に使うための心構え

非同期処理は「銀の弾丸」ではありません。むやみに使うと、コードは複雑化し、デバッグは困難になります。

Java非同期処理を導入する際は、以下の心構えが大切です。

  1. 本当に非同期処理が必要か見極める。
  2. スレッド間で共有するデータは最小限にする。(可能な限り不変(Immutable)なオブジェクトを使う)
  3. 例外処理のパスを必ず実装する。
  4. スレッドプールのサイズは適切に設定する。(CPUコア数やI/O待機時間に応じて調整)

Java非同期処理を使いこなせれば、大規模なトラフィックをさばくスケーラブルなシステムを構築できる、ワンランク上のJavaエンジニアになれます。

マルチスレッド・非同期処理の設計パターンをさらに体系的に学びたい方には以下の書籍もおすすめです。

REVISIONS / 改訂履歴
REV.DATE内容
42026-10-04記載内容の事実確認と修正
32026-09-052026年9月時点の情報で全体を見直し
22026-06-232026年6月時点の情報で全体を見直し(古い記述を5件更新)
12025-10-25初版
  • この記事を書いた人
  • 最新記事

トム

SESで約8年、環境を変えられないもどかしさの中にいました。32歳で自社開発企業に転職し、今はClaude Codeと自動化で、仕事も時間も組み直している途中です。
・Javaバックエンドエンジニア(Java歴12年以上)
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎
詳細なプロフィール

-Java入門