「非同期って何をしているの?」

まず、 普通のJavaプログラムを考えてみます。

step1();
step2();
step3();

基本的には、 step1() が終わってから、 step2()、 その後に step3() が実行されます。

これは、 同期的な処理 と考えることができます。

同期処理のイメージ

step1

step2

step3

一方で、 ある処理を別のスレッドで実行して、 元の処理を先に進めることもできます。

非同期処理のイメージ

メイン処理

処理Aを開始

メイン処理は次へ

└─ 処理Aは別で実行中

非同期処理は「処理を待たずに先へ進める」ための考え方

まずは同期処理を理解する

非同期処理を理解するには、 まず同期処理との違いを見るのが分かりやすいです。

System.out.println("A");


doSomething();


System.out.println("B");

この場合、 doSomething() が終わるまで、 "B" は出力されません。

A 先に実行
doSomething() 終わるまで待つ
B 処理終了後に実行

これは悪いことではありません。

むしろ、 処理の順番が明確なので、 通常の業務処理では同期処理が基本になることも多いです。

非同期にすれば必ず速くなるわけではない

ここは意外と重要です。

非同期処理は、 「待っている時間を有効に使う」 「複数の独立した処理を並行して進める」 といった場面で効果を発揮します。

Thread。処理を別のスレッドで動かす

Javaには、 処理を別スレッドで実行するための Thread があります。

Thread thread = new Thread(() -> {

    System.out.println("別スレッドで実行");

});

thread.start();

ここで重要なのは、 run() ではなく、 start() を呼んでいることです。

run() 通常のメソッド呼び出し
start() 新しいスレッドで処理を開始

ただし、 実際の業務システムで new Thread() を大量に書くことは、 あまり一般的ではありません。

スレッドを自分で管理するのではなく、 スレッドを管理してくれる仕組みを利用することが多くなります。

ExecutorService。スレッドを管理する

複数の処理を非同期で実行したい場合、 ExecutorService を使う方法があります。

ExecutorService executor =
        Executors.newFixedThreadPool(3);


executor.submit(() -> {

    System.out.println("処理を実行");

});


executor.shutdown();

ここでは、 自分でスレッドを作るのではなく、 Executorに処理を渡しています。

イメージ

ExecutorService

スレッドを管理

渡された処理を実行

実際の業務コードを読むときは、

「このExecutorは何個くらいの処理を並行して動かすためのものか?」

という視点を持つと理解しやすくなります。

スレッド数は多ければいいわけではない

スレッドを増やせば、 必ず処理性能が上がるわけではありません。

CPU、メモリ、DB接続数、 外部APIなどとの関係もあるため、 実際のシステムでは適切な設計が必要です。

Future。非同期処理の結果を受け取る

非同期処理を開始したあと、 「その処理の結果が欲しい」 という場面があります。

そこで登場するのが Future です。

Future<Integer> future =
        executor.submit(() -> {

            return 100;

        });


Integer result = future.get();

submit() で処理を開始し、 Future を受け取ります。

そして、 get() で結果を取得します。

Futureのイメージ

非同期処理を開始

Futureを受け取る

処理が終わる

get()で結果を取得

ただし、 get() を呼んだ時点で結果がまだ出ていなければ、 その場で待つことになります。

非同期で開始しても、結果を待つ場所では待つ

この感覚は、 非同期処理を理解するうえでかなり重要です。

CompletableFuture。非同期処理をつなげる

Javaの業務コードで、 特に目にすることがあるのが CompletableFuture です。

Futureだけでは、 「処理が終わったら次の処理をする」 という流れを扱いにくい場合があります。

CompletableFutureでは、 非同期処理の後続処理をつなげて書くことができます。

CompletableFuture
    .supplyAsync(() -> {

        return "Hello";

    })
    .thenApply(value -> {

        return value + " Java";

    })
    .thenAccept(value -> {

        System.out.println(value);

    });

処理の流れは、

処理の流れ

supplyAsync

"Hello"

↓ thenApply
"Hello Java"

↓ thenAccept
出力

つまり、 CompletableFuture は、

「非同期処理と、その後の処理をつなげて表現する」

ための仕組みとして見ると分かりやすいです。

supplyAsyncとrunAsyncの違い

CompletableFuture には、 supplyAsync()runAsync() があります。

supplyAsync() 戻り値を持つ非同期処理
runAsync() 戻り値を持たない非同期処理

supplyAsync() は、 非同期処理の結果を後続処理へ渡したい場合に使います。

CompletableFuture<User> future =
        CompletableFuture.supplyAsync(() -> {

            return findUser();

        });

この場合、 findUser() の戻り値である User が、 CompletableFuture<User> として扱われます。

一方、 runAsync() は戻り値を必要としない処理を 非同期で実行する場合に使います。

CompletableFuture.runAsync(() -> {

    writeLog();

});

まずは、

戻り値が必要 → supplyAsync

戻り値が不要 → runAsync

と覚えておけば十分です。

thenApply。前の処理の結果を変換する

thenApply() は、 前の処理の結果を受け取り、 別の値へ変換するときに使います。

CompletableFuture<String> future =
        CompletableFuture
            .supplyAsync(() -> "100")
            .thenApply(value -> {

                return value + "円";

            });

ここでは、 "100" を受け取って、 "100円" に変換しています。

supplyAsync 値を作る
thenApply 値を変換する

これは、 Streamの map() と少し似た感覚で捉えることもできます。

Streamでは、 データを変換するために map() を使いました。

CompletableFutureでは、 前の非同期処理の結果を変換するために thenApply() を使います。

複数の非同期処理を組み合わせる

実際のシステムでは、 一つの処理だけを非同期で実行するとは限りません。

例えば、 「ユーザー情報」と「注文情報」を 別々に取得したい場合があります。

CompletableFuture<User> userFuture =
        CompletableFuture.supplyAsync(() -> {
            return findUser();
        });


CompletableFuture<Order> orderFuture =
        CompletableFuture.supplyAsync(() -> {
            return findOrder();
        });

それぞれ独立した処理であれば、 並行して進めることができます。

イメージ

userFuture

ユーザー情報取得

orderFuture

注文情報取得


両方の結果を利用

こうした複数処理の組み合わせでは、 thenCombine() などが利用できます。

userFuture.thenCombine(
    orderFuture,
    (user, order) -> {

        return createResult(user, order);

    }
);

ここで重要なのは、 メソッド名を暗記することではありません。

「この2つの非同期処理が終わったあと、何をするのか?」

という処理の流れを見ることです。

非同期処理のエラーをどう扱うのか

非同期処理でも、 もちろんエラーは発生します。

CompletableFuture では、 exceptionally() などを使って エラー時の処理をつなげることができます。

CompletableFuture
    .supplyAsync(() -> {

        return callApi();

    })
    .exceptionally(e -> {

        System.out.println("エラー発生");

        return null;

    });

通常の処理だけを見ると、 非同期処理は簡単に見えます。

しかし実際の業務システムでは、 むしろ、

「失敗した場合にどうなるのか?」

を確認することが重要です。

業務コードを読むときの注意

非同期処理を見つけたら、 成功時の処理だけでなく、

・例外をどこで処理しているか
・失敗した場合に何を返すか
・呼び出し元がどう判断するか

まで確認すると安全です。

allOf。複数の処理が終わるのを待つ

複数の非同期処理を開始したあと、 「全部終わってから次へ進みたい」 というケースがあります。

そのような場合に、 CompletableFuture.allOf() を利用できます。

CompletableFuture<Void> all =
        CompletableFuture.allOf(
            userFuture,
            orderFuture
        );


all.thenRun(() -> {

    System.out.println("すべて完了");

});
イメージ

userFuture ──┐
orderFuture ─┼→ allOf → 次の処理
productFuture ─┘

つまり、 複数の処理を並行して開始し、 全部終わったことを確認してから 次の処理へ進む、 という流れを作ることができます。

CompletableFutureを見たら、まず処理の流れを追う

実際の業務コードでは、 CompletableFuture が何段にもつながっていることがあります。

CompletableFuture
    .supplyAsync(() -> findUser())
    .thenApply(user -> createRequest(user))
    .thenApply(request -> callApi(request))
    .thenAccept(result -> saveResult(result))
    .exceptionally(e -> {

        logError(e);

        return null;

    });

最初は、 これを一気に理解しようとしなくて大丈夫です。

読み方

supplyAsync
→ 最初に何を取得・実行している?

thenApply
→ 結果を何に変換している?

thenAccept
→ 最終的に何をしている?

exceptionally
→ エラー時はどうなる?

Streamの記事でも、 「処理を一つずつ追う」 という考え方を紹介しました。

非同期処理も同じです。

「次に何が実行されるのか?」を追えばいい

メソッド名を全部暗記するより、 処理の流れを理解することの方が重要です。

今回のポイント

Javaの非同期処理は、 単純に「別スレッドで動かす」という話だけではありません。

実際には、

非同期処理の基本

同期処理
→ 処理が終わるまで待つ

Thread
→ 別スレッドで処理を実行する

ExecutorService
→ スレッドを管理して処理を実行する

Future
→ 非同期処理の結果を受け取る

CompletableFuture
→ 非同期処理と後続処理をつなげる

そして、 CompletableFuture を読むときは、

supplyAsync 非同期処理を開始する
thenApply 結果を変換する
thenAccept 結果を受け取って処理する
thenCombine 複数の処理を組み合わせる
exceptionally エラー時の処理をする
allOf 複数の処理の完了を待つ

最初から、 すべてのAPIを覚える必要はありません。

まずは、

「どの処理を非同期で動かして、次に何をしているのか?」

という視点を持つこと。

これだけでも、 実際の業務システムで見かける CompletableFuture のコードは、かなり読みやすくなります。

非同期処理は、 「難しい構文を覚える」よりも、 処理の流れと待つ場所を理解する ことが大切です。