[Java]FutureパターンのイメージとExecutorSeviceを使ったコード
Futureパターンとは
Futureパターンとは、Javaのデザインパターンの1つです。別スレッドに仕事を依頼し、引換券(Future)を受け取り、任意のタイミングで実行結果を受け取ります。
"Future"は"引換券"
Futureパターンの"Future"とは"未来"、"先物"という意味ですが、ここでは"引換券"と略したほうが理解がしやすいです。
牛丼屋で例えてみる
あなたは牛丼屋さんに行きました。券売機で食券を購入するタイプのお店です。
ここで牛丼を注文しました。券売機からは整理番号と商品名が書かれた食券が出てきます。券売機の購入データは厨房に送られ、店員さんが牛丼を作り始めます。その間にあなたは席を確保したりスマホをチェックしたりします。料理ができた頃合いに配膳口に行き、チケットを渡します。料理ができていれば受け取り、できていなければできるまでその場で待ちます。(不親切なことに、料理が出来上がっても厨房からは呼び出されないシステムです)
この例えの、食券がFutureパターンにおけるFutureオブジェクトの役割です。Futureオブジェクトは、結果を受け取るための引換券です。
実際にFutureパターンを使用したい場面
Futureパターンが有効なとき、それは時間のかかる処理があるときです。時間のかかる処理とは、例えばAPIの呼び出しやSQLの発行などです。それらを別スレッドで行わせておき、その間メインのスレッドは別の作業を行い、必要になったタイミングで結果を受け取れればスループットが向上します。また、複数APIを並列に呼び出したい場合、それらをスレッドに割り当て、結果を待つ場合にも使用できます。
ExecutorService
別スレッドにタスクを依頼する場合、メインスレッドから別スレッドを直接作成する方法もありますが、ここではExecutorServiceを使います。ExecutorServiceでは、スレッドの作成や管理を行ってくれます。
Futureパターンのシーケンス図
Futureパターンのシーケンス図は以下のようになります。
- メインスレッドはExecutorServiceに別スレッドで行ってほしいタスクを渡します。
- ExecutorServiceはFutureを作成し、タスクとFutureを紐づけます。スレッドを起動します(すぐ起動せず、にキューに入れる実装などもあります)。
- メインスレッドにFutureが返されます。
- ExecutorServiceにより起動されたスレッドでは、メインから渡された処理が実行され、処理が終わったらFutureに値をセットします。
- メインスレッドはFutureに対し
get
を呼び出します。処理が終わっていたら処理結果を受け取ります。処理が終わっていなければ終わるまで待った後、計算結果を受け取ります。
コード(別スレッドが1つ)
ExecutorServiceを使用したコード例です。
呼び出し側のコードです。
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();
}
}
}
別スレッドで行ってほしいタスクのコードです。
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()
で値が受け取れていることが分かります。
コード(別スレッドが複数)
メインスレッドから複数のスレッドを起動する方法です。
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個作成し、並列に動かそうとしたコードですが意図したとおりに動きません。
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
submit
→get
→submit
→get
という順番で呼び出しているので、並列に処理を走らせることができていません。
あとがき
"うまく動かない例"を社内で発見したのが、今回の記事を書いたきっかけです。同じ間違いが起こらないように祈ってます。
Discussion