Javaの非同期処理、概念はわかるのに「いざ実装すると手が止まる」という方は多いです。
この記事では、Thread・ExecutorService・Future・CompletableFutureの4つの手法を、コピペで動くサンプルコード付きで解説します。
10年以上Javaで開発してきた私が、現場で実際に使い分けている判断基準もあわせて紹介します。
「同期処理との違い」「どの手法を選ぶべきか」「デッドロックや例外処理の落とし穴」まで、この1記事でJavaの非同期処理の全体像がつかめます。
Javaの非同期処理とは?【基本をわかりやすく解説】

Javaの非同期処理は、システムのパフォーマンスと応答性を向上させる上で欠かせない技術です。まずは、その基本的な考え方から見ていきましょう。
非同期処理と同期処理の違い
私たちのプログラムは通常、「同期処理」で動いています。
同期処理とは、タスクAが終わるまでタスクBを開始しない、直線的な処理方法です。
スーパーのレジで、前の人の会計が完全に終わるまで自分の会計が始まらないのと同じです。
シンプルで分かりやすい反面、時間のかかる処理(タスクA)があると、後続の処理(タスクB)がすべて待たされてしまいます。
対して非同期処理は、タスクAの完了を待たずに、先にタスクBを開始できる処理方法を指します。
レストランで料理を注文(タスクA)した後、ウェイターは料理が出来上がるのを待たずに、別のテーブルの注文を取りに行きます(タスクB)。
Javaの非同期処理では、時間のかかる処理を別の作業員(スレッド)に任せ、メインの作業員は待たずに次の仕事に進むイメージです。
これにより、システム全体としての処理能力(スループット)が向上します。
なぜ非同期処理が必要なのか
非同期処理の最大の目的は、ユーザー体験の向上とシステムリソースの有効活用です。
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つの手法を、進化の順に解説します。
スレッド(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非同期処理の実装例【コピペで動くサンプルコード】

理論だけでは分かりにくいので、Java非同期処理の具体的なコード例を見てみましょう。
スレッドを使った基本コード例
Runnableインタフェースを使い、新しいスレッドで処理を実行する最もシンプルな例です。
実行すると、メインスレッドの「終了」が、別スレッドの「終了」よりも先に表示されるはずです。
ExecutorService+Futureの実装例
スレッドプールを使い、Callableで結果を取得する例です。
future.get()で結果を受け取れますが、その時点で処理が終わっていないと待機が発生します。
CompletableFutureで並列処理を行う例
CompletableFutureを使い、2つのAPI呼び出しを並列実行し、結果を結合するモダンな例です。
処理A(2秒)と処理B(3秒)が並列で動くため、get()で待つ実装と比べて、システム全体は約3秒で完了します(逐次なら2+3=5秒)。
非同期処理の結果を待つ3つの方法|get・join・コールバック
非同期処理の結果をいつ・どうやって受け取るかは、実装で迷いやすいポイントです。主に3つの方法があります。
Future.get(): 結果が返るまで呼び出し元スレッドをブロックします。タイムアウトを指定でき(get(5, TimeUnit.SECONDS))、処理が長すぎる場合にTimeoutExceptionで打ち切れます。CompletableFuture.join():get()と同様にブロックしますが、検査例外(ExecutionException)をスローしない点が異なります。try-catchを書きたくない場面で便利です。thenAccept()/thenApply()(コールバック): 結果が出たら自動で次の処理を実行するノンブロッキング方式。メインスレッドを止めたくないWebアプリで推奨されます。
判断基準はシンプルです。メインスレッドを止めてよいならjoin()、止めたくないならthenAccept()系のコールバックを使いましょう。
Javaの非同期処理を使うメリットと注意点

Java非同期処理は強力ですが、正しく使わないと予期せぬ不具合を生みます。メリットとリスクを理解しましょう。
パフォーマンス向上とスケーラビリティ
非同期処理の最大のメリットは、システムの応答性(レスポンス)と処理能力(スループット)の向上です。
特に、データベースアクセスや外部API呼び出しといった、結果の到着を待っている時間(I/O待機)が多い処理で絶大な効果を発揮します。
待機時間にスレッドを解放し、別のリクエスト処理に回せるため、少ないリソースで多くのリクエストをさばけるようになります(スケーラビリティの向上)。
また、CPUをフルに使う計算処理(CPUバウンド)であっても、マルチコアCPUの各コアに処理を割り当てる「並列処理」を行うことで、全体の処理時間を短縮可能です。
デッドロックや競合状態などのリスク
非同期処理(マルチスレッド)を導入する上で、最も注意すべきリスクが2つあります。
これらの問題は「スレッドセーフティ」と呼ばれ、Java非同期処理を扱う上で最も難解な部分です。
例外処理やエラーハンドリングのコツ
非同期処理の例外は、呼び出し元のスレッドに直接伝わらないため、ハンドリングを忘れがちです。
Threadで例外がスローされると、そのスレッドは停止しますが、メインスレッドには通知されません。UncaughtExceptionHandlerで捕捉する必要があります。Futureの場合、非同期処理中の例外はfuture.get()を呼び出したタイミングでExecutionExceptionとしてスローされます。get()をtry-catchで囲む必要があります。CompletableFutureでは、exceptionally()やhandle()といった専用のメソッドチェーンを使って、エラー処理を記述するのがベストプラクティスです。
エラーハンドリングを怠ると、処理が失敗しているのにシステムは正常に動き続けているように見え、デバッグが困難になります。
実務上のTipsとして、非同期処理では必ずログにスレッド名を出力するようにしましょう。
Thread.currentThread().getName()をログに含めておくと、障害調査時にどのスレッドで例外が発生したかを追跡できます。
私はこの習慣がなかったために、複数スレッドの例外ログが混在し原因特定に苦労した経験があります。
実践的な活用シーンとベストプラクティス

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非同期処理を導入する際は、以下の心構えが大切です。
- 本当に非同期処理が必要か見極める。
- スレッド間で共有するデータは最小限にする。(可能な限り不変(Immutable)なオブジェクトを使う)
- 例外処理のパスを必ず実装する。
- スレッドプールのサイズは適切に設定する。(CPUコア数やI/O待機時間に応じて調整)
Java非同期処理を使いこなせれば、大規模なトラフィックをさばくスケーラブルなシステムを構築できる、ワンランク上のJavaエンジニアになれます。
マルチスレッド・非同期処理の設計パターンをさらに体系的に学びたい方には以下の書籍もおすすめです。
| REV. | DATE | 内容 |
|---|---|---|
| 4 | 2026-10-04 | 記載内容の事実確認と修正 |
| 3 | 2026-09-05 | 2026年9月時点の情報で全体を見直し |
| 2 | 2026-06-23 | 2026年6月時点の情報で全体を見直し(古い記述を5件更新) |
| 1 | 2025-10-25 | 初版 |



