エンジニアのはしがき

プログラミングの日々の知見を書き連ねているブログです

Javaでの非同期処理の方法についてピックアップした

まだまだJavaの勉強中です。 先日、そういえばJavaで非同期処理する方法をまだ知らなかったなぁと思い調べてみました。

JavaScript, C#のようにJavaでもasync, awaitで非同期処理が書けるはずと思っていましたが残念ながら書けないようです。

勿論、非同期処理は別の書き方がいくつか存在するようなので、使いやすいと思われるものをピックアップしました。

Threadを使った非同期処理

自分でThreadクラスを継承したクラスを定義し内部に非同期処理を記述した上で、実行時にインスタンス化する方法です。 JDK1.0から利用可能なのでレガシーな環境なら使っても良いかもしれませんが、基本的には後述の方法の方が多機能なのであえて使う必要はなさそうです。

// 非同期処理はThreadを継承したクラスを定義し、run()に具体的な処理を記述する
class OriginalThread extends Thread {
    public void run() {
        try {
            Thread.sleep(5000);
            System.out.println("thread completed!");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class ThreadSample {
    public static void main() throws InterruptedException {
        // 上記で定義したThreadを継承したクラスのインスタンスを生成。
        OriginalThread originThread = new OriginalThread();
        // 非同期処理を開始。
        originThread.start();
        System.out.println("start...");
        // join()で非同期処理の完了まで待機する。
        originThread.join();
        System.out.println("all completed!");
    }
}

Executorを使った非同期処理

JDK1.5から実装された方法です。

Executorはスレッドプールの実装を持っており、複数の非同期処理をさばくにあたって便利な機能を持っています。

下記はシングルスレッドで非同期処理を実行させるサンプルですが、スレッド数を明示的に指定して実行させたり(Executors. newFixedThreadPool())、利用可能なスレッドを再利用させること(Executors.newCachedThreadPool())も可能です。

参考:Executors (Java SE 17 & JDK 17)

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class ExecutorSample {
    public static void main() throws InterruptedException, ExecutionException {
        // スレッドプール利用のためにExecutorServiceを生成。今回はnewSingleThreadExecutor()を呼ぶことでシングルスレッドで非同期処理させる。
        ExecutorService pool = Executors.newSingleThreadExecutor();
        try {
            // 実行したい非同期処理をCallable(またはRunnable)インターフェースを実装した無名クラスで定義する。
            Callable<String> printlnAsync = new Callable<String>() {
                // Callableの場合、call()をオーバーライドして具体的処理を記述する
                @Override
                public String call() throws InterruptedException {
                    Thread.sleep(5000);
                    return "callable completed!";
                }
            };            
            System.out.println("start...");
            // submit()でCallable(or Runnable)を渡すことで非同期処理結果を取得するためのFutureが戻り値として取得できる
            Future<String> printlnAsyncResult = pool.submit(printlnAsync);
            // future.get()でCallableの戻り値を取得する。取得完了まで処理は待機する。
            System.out.println(printlnAsyncResult.get());
            System.out.println("all completed!");
        }
        finally{
            // スレッドプールへのタスク受付を終了させる。明示的に記述しないとタスク受付のスレッドが残ってしまう。
            pool.shutdown();
        }
    }
}

この方法では、最初に生成したExecutorServiceを必ずshutdown()させておく必要があります。shutdown()を忘れると、スレッドプールへのタスク受付の為のスレッドが残存してしまうので注意です。

CompletableFutureを使った非同期処理

こちらはJDK1.8からの実装です。

複数の非同期処理のハンドリングに長けており、JavaScriptPromise.all()のように複数の非同期処理を並列に繋いで結果を取得したりするといったことがしやすくなっています。非同期処理を頻繁に使い、実行順の管理が求められるような場合は利用候補になるかもしれません。

参考: CompletableFuture (Java SE 17 & JDK 17)

下記は3つの非同期処理を並行に繋ぎ、全ての完了を待機するケースです。

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class CompletableFutureSample {
    // 非同期処理をメソッドに定義する
    private static String prinlnAsync(String str) {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(str);
        return str + "completed!";
    }

    public static void main() throws InterruptedException, ExecutionException {
        // 複数の非同期処理をListに格納する
        List<CompletableFuture<String>> futureList = Arrays.asList(
            CompletableFuture.supplyAsync(() -> prinlnAsync("async1")),
            CompletableFuture.supplyAsync(() -> prinlnAsync("async2")),
            CompletableFuture.supplyAsync(() -> prinlnAsync("async3"))
        );
        // 全ての非同期処理をCompletableFuture<Void>にまとめる
        CompletableFuture<Void> future = CompletableFuture.allOf(futureList.toArray(new CompletableFuture[futureList.size()]));
        // 全ての非同期処理が完了した際の処理を記述する
        future.whenComplete((result, e) -> {
            // 例外のハンドリング
            if (e != null) {
                System.err.println(e);
                return;
            }
            System.out.println("all async completed!");
        });
        // 非同期処理完了まで待機する
        future.get();
    }   
}

supplyAsync()は引数にSupplier<T>を渡せるメソッドで、戻り値にCompletableFuture<T>を持ちます。上記のサンプルでは戻り値を持つ非同期メソッドを引数に渡しています。

3つの非同期処理は並列実行される為、実行毎に順番が保証されず、標準出力の出力結果も毎回異なります。

例: 1回目実行時の標準出力

async2
async1
async3
all async completed!

例: 2回目実行時の標準出力

async3
async2
async1
all async completed!

あとがき

今回は純粋なJavaでの非同期処理をまとめましたが、JavaフレームワークであるSpring Bootを利用すると、メソッドに対して@Asyncアノテーションを付与するだけで非同期で動作するメソッドが書けるそうです。 Spring Boot @Async アノテーションで非同期メソッドの作成 - 公式サンプルコード

C#, JavaScriptasync風な書き方が出来るのでSpring Bootが使える環境なら、個人的にはこちらで書いていきたい…。

参考

JavaでThreadを使いこなす!生成から待ち合わせまで非同期処理の方法 | Javaコラム

Executorフレームワークの使い方 | Java好き

Javaの並列処理いろいろ | SIOS Tech. Lab

CompletableFutureでJavaの非同期処理を試す - 技術メモ