Overview
ExecutorServiceは、非同期モードでのタスク実行を簡略化するJDKのAPIです。 一般的にExecutorServiceは、スレッドのプールと、そこにタスクを割り当てるためのAPIを自動的に提供します。
Further reading:
Guide to the Fork/Join Framework in Java
java.util.concurrentの概要
ExecutorServiceのインスタンス化
2.1. Executors クラスのファクトリーメソッド
ExecutorService を作成する最も簡単な方法は、Executors クラスのファクトリーメソッドを使用することです。
例えば、以下のコードは 10 個のスレッドを持つスレッドプールを作成します。 ニーズに合った最適な方法を見つけるには、Oracleの公式ドキュメントを参照してください。
2.2. ExecutorServiceを直接作成する
ExecutorServiceはインターフェイスであるため、その実装のインスタンスを使用することができます。
たとえば、ThreadPoolExecutor クラスには、エクゼキュータ サービスとその内部プールを構成するために使用できるいくつかのコンストラクタがあります:
ExecutorService executorService = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
上記のコードは、ファクトリー メソッド newSingleThreadExecutor() のソース コードと非常によく似ていることに気付くかもしれません。
Assigning Tasks to the ExecutorService
ExecutorService は Runnable と Callable のタスクを実行できます。 この記事では、シンプルにするために、2つのプリミティブなタスクを使用します。
Runnable runnableTask = () -> { try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }};Callable<String> callableTask = () -> { TimeUnit.MILLISECONDS.sleep(300); return "Task's execution";};List<Callable<String>> callableTasks = new ArrayList<>();callableTasks.add(callableTask);callableTasks.add(callableTask);callableTasks.add(callableTask);
Executorインターフェースから継承したexecute()をはじめ、submit()、invokeAny()、invokeAll()などのメソッドを使って、ExecutorServiceにタスクを割り当てることができます。
execute()メソッドはボイドであり、タスクの実行結果を取得したり、タスクの状態(実行中かどうか)を確認することはできません:
executorService.execute(runnableTask);
submit()はCallableまたはRuncableタスクをExecutorServiceにサブミットし、Future型の結果を返します。
Future<String> future = executorService.submit(callableTask);
invokeAny()は、タスクの集合体をExecutorServiceに割り当て、それぞれを実行させ、1つのタスクの実行が成功した場合はその結果を返します。
String result = executorService.invokeAny(callableTasks);
invokeAll()は、タスクのコレクションをExecutorServiceに割り当て、それぞれを実行させ、すべてのタスクの実行結果をFuture型のオブジェクトのリストの形で返します。
List<Future<String>> futures = executorService.invokeAll(callableTasks);
先に進む前に、次の2つの項目について説明する必要があります:ExecutorServiceのシャットダウンとFutureの戻り値の処理です。
Shutting Down an ExecutorService
一般的に、処理すべきタスクがない場合、ExecutorServiceは自動的には破棄されません。
アプリが不定期に現れるタスクを処理する必要がある場合や、タスクの量がコンパイル時にわからない場合など、場合によってはこれは非常に便利です。
一方で、アプリが終了しても、待機しているExecutorServiceがJVMの実行を継続するため、アプリが停止しないこともあります。
shutdown()メソッドは、ExecutorServiceをすぐには破壊しませんが、新しいタスクの受け入れを停止し、実行中のすべてのスレッドが現在の作業を終えた後にシャットダウンします。
executorService.shutdown();
shutdownNow()メソッドはExecutorServiceを即座に破壊しようとしますが、すべての実行中のスレッドが同時に停止することは保証されません:
List<Runnable> notExecutedTasks = executorService.shutDownNow();
このメソッドは、処理を待っているタスクのリストを返します。
ExecutorServiceを停止する良い方法のひとつとして、これらのメソッドとawaitTermination()メソッドを組み合わせて使用する方法があります。
The Future Interface
submit()およびinvokeAll()メソッドは、Future型のオブジェクトまたはオブジェクトのコレクションを返します。これにより、タスクの実行結果を取得したり、タスクの状態(実行中かどうか)を確認したりすることができます。
Future インターフェイスには特別なブロッキング メソッド get() があり、Callable タスクの実行結果を実際に返すか、Runnable タスクの場合は null を返します。
Future<String> future = executorService.submit(callableTask);String result = null;try { result = future.get();} catch (InterruptedException | ExecutionException e) { e.printStackTrace();}
タスクがまだ実行中に get() メソッドを呼び出すと、タスクが適切に実行されて結果が得られるまで実行がブロックされます。
get()メソッドによる非常に長いブロッキングにより、アプリケーションのパフォーマンスが低下する可能性があります。
String result = future.get(200, TimeUnit.MILLISECONDS);
実行期間が指定された時間(ここでは200ミリ秒)よりも長い場合、TimeoutExceptionがスローされます。
割り当てられたタスクがすでに処理されたかどうかを確認するには、isDone()メソッドを使用します。
Futureのインターフェイスでは、cancel()メソッドでタスクの実行をキャンセルしたり、isCancelled()メソッドでキャンセルをチェックすることもできます:
boolean canceled = future.cancel(true);boolean isCancelled = future.isCancelled();
ScheduledExecutorServiceインターフェイス
ScheduledExecutorServiceは、あらかじめ定義された遅延時間の後、または定期的にタスクを実行します。
繰り返しになりますが、ScheduledExecutorServiceをインスタンス化する最も良い方法は、Executorsクラスのファクトリーメソッドを使用することです。
このセクションでは、1つのスレッドを持つScheduledExecutorServiceを使用します。
2つのscheduled()メソッドにより、RunnableまたはCallableタスクを実行することができます:
Future<String> resultFuture = executorService.schedule(callableTask, 1, TimeUnit.SECONDS);
scheduledAtFixedRate()メソッドにより、一定の遅延時間後にタスクを定期的に実行することができます。 上のコードでは、callableTaskを実行する前に1秒間遅延させています。
次のコードブロックでは、最初に100ミリ秒の遅延が発生した後、タスクを実行します。
Future<String> resultFuture = service .scheduleAtFixedRate(runnableTask, 100, 450, TimeUnit.MILLISECONDS);
プロセッサが割り当てられたタスクを実行するのに、scheduleAtFixedRate()メソッドの期間パラメータよりも長い時間が必要な場合、ScheduledExecutorServiceは、現在のタスクが完了するまで待ってから次のタスクを開始します。
タスクの繰り返しの間に固定長の遅延が必要な場合は、scheduledWithFixedDelay()を使用する必要があります。
たとえば、次のコードは、現在の実行の終了と次の実行の開始との間に150ミリ秒の休止を保証します:
service.scheduleWithFixedDelay(task, 100, 150, TimeUnit.MILLISECONDS);
schedulAtFixedRate()およびschedulWithFixedDelay()メソッドのコントラクトに従って、タスクの期間実行は、ExecutorServiceの終了時またはタスク実行中に例外がスローされた場合に終了します。
ExecutorService vs Fork/Join
Java 7のリリース後、多くの開発者がExecutorServiceフレームワークをfork/joinフレームワークに置き換えることを決定しました。
ExecutorServiceは、生成されるスレッドの数や、別々のスレッドで実行されるべきタスクの粒度を制御する能力を開発者に与えます。
ExecutorService を使用すると、開発者は生成されたスレッドの数と、別々のスレッドで実行するタスクの粒度を制御することができます。ExecutorService の最適な使用例は、「1 つのタスクに 1 つのスレッド」というスキームに従って、トランザクションやリクエストなどの独立したタスクを処理することです。
おわりに
ExecutorService は比較的シンプルですが、いくつかの一般的な落とし穴があります。
未使用のExecutorServiceを存続させる:ExecutorServiceの停止方法については、セクション4で詳しく説明しています。 アプリケーションが効率的にタスクを実行するために、いくつのスレッドが必要かを判断することは非常に重要です。 スレッドプールが大きすぎると、ほとんどが待機モードになるスレッドを作成するために、不必要なオーバーヘッドが発生します。
タスクがキャンセルされた後に Future の get() メソッドを呼び出すこと。
Future の get() メソッドでの予期せぬ長時間のブロッキング。
いつものように、この記事のコードはGitHubリポジトリで公開されています。
Learn Springコースを通じて、Spring 5とSpring Boot 2を始めましょう:
>> CHECK OUT THE COURSE