💫

[Java]FutureパターンのイメージとExecutorSeviceを使ったコード

2021/12/25に公開

Futureパターンとは

Futureパターンとは、Javaのデザインパターンの1つです。別スレッドに仕事を依頼し、引換券(Future)を受け取り、任意のタイミングで実行結果を受け取ります。

"Future"は"引換券"

Futureパターンの"Future"とは"未来"、"先物"という意味ですが、ここでは"引換券"と略したほうが理解がしやすいです。

牛丼屋で例えてみる

あなたは牛丼屋さんに行きました。券売機で食券を購入するタイプのお店です。

ここで牛丼を注文しました。券売機からは整理番号と商品名が書かれた食券が出てきます。券売機の購入データは厨房に送られ、店員さんが牛丼を作り始めます。その間にあなたは席を確保したりスマホをチェックしたりします。料理ができた頃合いに配膳口に行き、チケットを渡します。料理ができていれば受け取り、できていなければできるまでその場で待ちます。(不親切なことに、料理が出来上がっても厨房からは呼び出されないシステムです)

この例えの、食券がFutureパターンにおけるFutureオブジェクトの役割です。Futureオブジェクトは、結果を受け取るための引換券です。

実際にFutureパターンを使用したい場面

Futureパターンが有効なとき、それは時間のかかる処理があるときです。時間のかかる処理とは、例えばAPIの呼び出しやSQLの発行などです。それらを別スレッドで行わせておき、その間メインのスレッドは別の作業を行い、必要になったタイミングで結果を受け取れればスループットが向上します。また、複数APIを並列に呼び出したい場合、それらをスレッドに割り当て、結果を待つ場合にも使用できます。

ExecutorService

別スレッドにタスクを依頼する場合、メインスレッドから別スレッドを直接作成する方法もありますが、ここではExecutorServiceを使います。ExecutorServiceでは、スレッドの作成や管理を行ってくれます。

Futureパターンのシーケンス図

Futureパターンのシーケンス図は以下のようになります。

  1. メインスレッドはExecutorServiceに別スレッドで行ってほしいタスクを渡します。
  2. ExecutorServiceはFutureを作成し、タスクとFutureを紐づけます。スレッドを起動します(すぐ起動せず、にキューに入れる実装などもあります)。
  3. メインスレッドにFutureが返されます。
  4. ExecutorServiceにより起動されたスレッドでは、メインから渡された処理が実行され、処理が終わったらFutureに値をセットします。
  5. メインスレッドはFutureに対しgetを呼び出します。処理が終わっていたら処理結果を受け取ります。処理が終わっていなければ終わるまで待った後、計算結果を受け取ります。

コード(別スレッドが1つ)

ExecutorServiceを使用したコード例です。

呼び出し側のコードです。

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

public class Main {
  public static void main(String[] args) {
    // スレッドを1つ作成・管理するExecutorServiceを作成する
    ExecutorService service = Executors.newSingleThreadExecutor();
    // スレッドで行ってほしいタスク
    CallableTask task = new CallableTask("yucatio");

    try {
      // 
      Future<String> future = service.submit(task);

      // ここにメインスレッドで行う処理を書く

      try {
        // 結果の受け取り(処理が終わっていなければ待たされる)
        String result = future.get();

        System.out.println(result);
      } catch (InterruptedException | ExecutionException e) {
        e.printStackTrace();
      }
    } finally {
      service.shutdown();
    }
  }
}

別スレッドで行ってほしいタスクのコードです。

CallableTask.java
import java.util.concurrent.Callable;

public class CallableTask implements Callable<String> {
  private String name;

  public CallableTask(String name) {
    this.name = name;
  }

  @Override
  public String call() throws Exception {
    System.out.println("Thread start");
    // ここに別スレッドで行いたい処理を書く
    // 今回はsleepで代用
    try{
      Thread.sleep(1000);
    }catch(InterruptedException e){
      System.out.println(e);
    }
    System.out.println("Thread end");
    return "Hello " + name;
  }
}

こちらを実行するとこのように表示されます。

Thread start
Thread end
Hello yucatio

submitされたタスクが実行され、future.get()で値が受け取れていることが分かります。

コード(別スレッドが複数)

メインスレッドから複数のスレッドを起動する方法です。

Main.java
public class Main {
  public static void main(String[] args) {
    // 複数スレッドを作成・管理するExecutorServiceを作成する
    ExecutorService service = Executors.newFixedThreadPool(2);
    // スレッドで行ってほしいタスクのリスト
    List<CallableTask> tasks = new ArrayList<>();
    tasks.add(new CallableTask("cookie"));
    tasks.add(new CallableTask("chocolate"));

    try {
      // 受け取ったFutureを格納するリスト
      List<Future<String>> futures = new ArrayList<>();
      for (CallableTask task: tasks) {

        Future<String> future = service.submit(task);
        futures.add(future);
      }

      for (Future<String> future : futures) {
        try {
          // 結果の受け取り(処理が終わっていなければ待たされる)
          String result = future.get();
  
          System.out.println(result);
        } catch (InterruptedException | ExecutionException e) {
          e.printStackTrace();
        }
      }
    } finally {
      service.shutdown();
    }
  }
}

こちらを実行するとこのように表示されます。

Thread start
Thread start
Thread end
Thread end
Hello cookie
Hello chocolate

スレッドが2個起動され、それぞれの結果を受け取ることができました。

うまく動かない例

こちらは上のコードを少し変えた例です。スレッドを2個作成し、並列に動かそうとしたコードですが意図したとおりに動きません。

Main.java
public class Main {
  public static void main(String[] args) {
    // 複数スレッドを作成・管理するExecutorServiceを作成する
    ExecutorService service = Executors.newFixedThreadPool(2);
    // スレッドで行ってほしいタスクのリスト
    List<CallableTask> tasks = new ArrayList<>();
    tasks.add(new CallableTask("cookie"));
    tasks.add(new CallableTask("chocolate"));

    try {
      for (CallableTask task: tasks) {
        Future<String> future = service.submit(task);

        try {
          // 結果の受け取り
          String result = future.get();
  
          System.out.println(result);
        } catch (InterruptedException | ExecutionException e) {
          e.printStackTrace();
        }
      }
    } finally {
      service.shutdown();
    }
  }
}

実行するとこのように表示されます。

Thread start
Thread end
Hello cookie
Thread start
Thread end
Hello chocolate

submitgetsubmitgetという順番で呼び出しているので、並列に処理を走らせることができていません。

あとがき

"うまく動かない例"を社内で発見したのが、今回の記事を書いたきっかけです。同じ間違いが起こらないように祈ってます。

参考文献

Java言語で学ぶデザインパターン入門(マルチスレッド編)

Discussion