Java ExecutorServiceのガイド
1. 概要
ExecutorServiceは、非同期モードでのタスクの実行を簡素化するJDKによって提供されるフレームワークです。 一般的に、ExecutorServiceは、タスクを割り当てるためのスレッドとAPIのプールを自動的に提供します。
参考文献:
JavaのFork / Joinフレームワークのガイド
Java 7で提供されるfork / joinフレームワークの紹介と、利用可能なすべてのプロセッサコアの使用を試みることで並列処理を高速化するツール。
java.util.concurrent.Locksのガイド
この記事では、Lockインターフェースのさまざまな実装と、Java 9 StampedLockクラスで新たに導入されたものを調べます。
2. ExecutorServiceのインスタンス化
2.1. Executorsクラスのファクトリメソッド
ExecutorServiceを作成する最も簡単な方法は、Executorsクラスのファクトリメソッドの1つを使用することです。
たとえば、次のコード行は、10個のスレッドを持つスレッドプールを作成します。
ExecutorService executor = Executors.newFixedThreadPool(10);
特定のユースケースを満たす事前定義されたExecutorServiceを作成するための他のいくつかのファクトリメソッドがあります。 ニーズに最適な方法を見つけるには、Oracle’s official documentationを参照してください。
2.2. ExecutorServiceを直接作成する
ExecutorServiceはインターフェースであるため、その実装のインスタンスを使用できます。 java.util.concurrentパッケージにはいくつかの実装から選択できますが、独自の実装を作成することもできます。
たとえば、ThreadPoolExecutorクラスには、エグゼキュータサービスとその内部プールを構成するために使用できるコンストラクタがいくつかあります。
ExecutorService executorService =
new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
上記のコードは、ファクトリメソッドnewSingleThreadExecutor().のsource codeと非常によく似ていることに気付くかもしれません。ほとんどの場合、詳細な手動構成は必要ありません。
3. ExecutorServiceへのタスクの割り当て
ExecutorServiceは、RunnableおよびCallableタスクを実行できます。 この記事で物事を単純にするために、2つの基本的なタスクを使用します。 ここでは、匿名の内部クラスの代わりにラムダ式が使用されていることに注意してください。
Runnable runnableTask = () -> {
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
};
Callable callableTask = () -> {
TimeUnit.MILLISECONDS.sleep(300);
return "Task's execution";
};
List> callableTasks = new ArrayList<>();
callableTasks.add(callableTask);
callableTasks.add(callableTask);
callableTasks.add(callableTask);
タスクは、Executorインターフェースから継承されるexecute()や、submit()、invokeAny(), invokeAll().など、いくつかの方法を使用してExecutorServiceに割り当てることができます。
execute()メソッドはvoid,であり、タスクの実行結果を取得したり、タスクのステータス(実行中か実行中か)を確認したりする可能性はありません。
executorService.execute(runnableTask);
submit()は、CallableまたはRunnableタスクをExecutorServiceに送信し、タイプFutureの結果を返します。
Future future =
executorService.submit(callableTask);
invokeAny()は、タスクのコレクションをExecutorService,に割り当て、それぞれを実行させ、1つのタスクが正常に実行された結果を返します(正常に実行された場合).
String result = executorService.invokeAny(callableTasks);
invokeAll()は、タスクのコレクションをExecutorService,に割り当て、それぞれを実行させ、すべてのタスク実行の結果をタイプFuture.のオブジェクトのリストの形式で返します。
List> futures = executorService.invokeAll(callableTasks);
ここで、先に進む前に、さらに2つのことについて説明する必要があります。ExecutorServiceのシャットダウンとFutureの戻り値の型の処理です。
4. ExecutorServiceのシャットダウン
一般に、処理するタスクがない場合、ExecutorServiceは自動的に破棄されません。 それは生き続け、新しい作業が行われるのを待ちます。
場合によっては、これは非常に役立ちます。たとえば、アプリが不規則に表示されるタスクを処理する必要がある場合や、これらのタスクの量がコンパイル時に不明な場合などです。
一方、アプリは終了する可能性がありますが、ExecutorServiceを待機すると、JVMが実行を継続するため、アプリは停止しません。
ExecutorServiceを適切にシャットダウンするために、shutdown()およびshutdownNow()APIがあります。
shutdown()メソッドは、ExecutorService.の即時破壊を引き起こしません。これにより、ExecutorServiceは新しいタスクの受け入れを停止し、実行中のすべてのスレッドが現在の作業を終了した後にシャットダウンします。
executorService.shutdown();
shutdownNow()メソッドはExecutorServiceをすぐに破棄しようとしますが、実行中のすべてのスレッドが同時に停止することを保証するものではありません。 このメソッドは、処理待ちのタスクのリストを返します。 これらのタスクをどうするかは開発者次第です。
List notExecutedTasks = executorService.shutDownNow();
ExecutorService(これはrecommended by Oracleでもあります)をシャットダウンする良い方法の1つは、これらの両方のメソッドをawaitTermination()メソッドと組み合わせて使用することです。 このアプローチでは、ExecutorServiceは最初に新しいタスクの実行を停止し、すべてのタスクが完了するまで指定された時間まで待機します。 その時間が経過すると、実行はすぐに停止します。
executorService.shutdown();
try {
if (!executorService.awaitTermination(800, TimeUnit.MILLISECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
executorService.shutdownNow();
}
5. Futureインターフェース
submit()およびinvokeAll()メソッドは、タイプFutureのオブジェクトまたはオブジェクトのコレクションを返します。これにより、タスクの実行結果を取得したり、タスクのステータスを確認したりできます(実行中かどうか)。または実行)。
Futureインターフェースは、Callableタスクの実行の実際の結果またはRunnableタスクの場合はnullを返す特別なブロッキングメソッドget()を提供します。 タスクの実行中にget()メソッドを呼び出すと、タスクが適切に実行されて結果が利用可能になるまで実行がブロックされます。
Future future = executorService.submit(callableTask);
String result = null;
try {
result = future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
get()メソッドが原因で非常に長いブロッキングが発生すると、アプリケーションのパフォーマンスが低下する可能性があります。 結果のデータが重要でない場合は、タイムアウトを使用することでこのような問題を回避できます。
String result = future.get(200, TimeUnit.MILLISECONDS);
実行期間が指定より長い場合(この場合は200ミリ秒)、TimeoutExceptionがスローされます。
isDone()メソッドを使用して、割り当てられたタスクがすでに処理されているかどうかを確認できます。
Futureインターフェイスは、cancel()メソッドを使用したタスク実行のキャンセル、およびisCancelled()メソッドを使用したキャンセルのチェックも提供します。
boolean canceled = future.cancel(true);
boolean isCancelled = future.isCancelled();
6. ScheduledExecutorServiceインターフェース
ScheduledExecutorServiceは、事前定義された遅延の後、および/または定期的にタスクを実行します。 繰り返しになりますが、ScheduledExecutorServiceをインスタンス化する最良の方法は、Executorsクラスのファクトリメソッドを使用することです。
このセクションでは、1つのスレッドを持つScheduledExecutorServiceが使用されます。
ScheduledExecutorService executorService = Executors
.newSingleThreadScheduledExecutor();
一定の遅延後に単一のタスクの実行をスケジュールするには、ScheduledExecutorServiceのscheduled()メソッドを使用します。 RunnableまたはCallableタスクを実行できる2つのscheduled()メソッドがあります。
Future resultFuture =
executorService.schedule(callableTask, 1, TimeUnit.SECONDS);
scheduleAtFixedRate()メソッドを使用すると、一定の遅延後に定期的にタスクを実行できます。 上記のコードは、callableTaskを実行する前に1秒間遅延します。
次のコードブロックは、100ミリ秒の初期遅延後にタスクを実行し、その後、450ミリ秒ごとに同じタスクを実行します。 プロセッサが割り当てられたタスクの実行にscheduleAtFixedRate()メソッドのperiodパラメータよりも多くの時間を必要とする場合、ScheduledExecutorServiceは現在のタスクが完了するまで待機してから、次のタスクを開始します。
Future resultFuture = service
.scheduleAtFixedRate(runnableTask, 100, 450, TimeUnit.MILLISECONDS);
タスクの反復間で固定長の遅延が必要な場合は、scheduleWithFixedDelay()を使用する必要があります。 たとえば、次のコードは、現在の実行の終了から別の実行の開始までの間に150ミリ秒の一時停止を保証します。
service.scheduleWithFixedDelay(task, 100, 150, TimeUnit.MILLISECONDS);
scheduleAtFixedRate()およびscheduleWithFixedDelay()メソッドコントラクトによると、タスクの期間実行は、ExecutorServiceの終了時、またはタスク実行中に例外がスローされた場合に終了します.
7. ExecutorService対。 Fork/Join
Java 7のリリース後、多くの開発者は、ExecutorServiceフレームワークをfork / joinフレームワークに置き換える必要があると判断しました。 ただし、これは常に正しい判断とは限りません。 使用が簡単であり、フォーク/結合に関連するパフォーマンスが頻繁に向上するにもかかわらず、開発者が同時実行を制御する量が減少します。
ExecutorServiceを使用すると、開発者は、生成されるスレッドの数と、個別のスレッドで実行する必要のあるタスクの粒度を制御できます。 ExecutorServiceの最適な使用例は、「1つのタスクに対して1つのスレッド」というスキームに従ったトランザクションや要求などの独立したタスクの処理です。
対照的に、according to Oracle’s documentation、fork / joinは、再帰的に小さな断片に分割できる作業を高速化するように設計されています。
8. 結論
ExecutorServiceは比較的単純ですが、いくつかの一般的な落とし穴があります。 それらを要約しましょう:
Keeping an unused ExecutorService alive:この記事のセクション4には、ExecutorServiceをシャットダウンする方法についての詳細な説明があります。
Wrong thread-pool capacity while using fixed length thread-pool:アプリケーションがタスクを効率的に実行するために必要なスレッド数を決定することは非常に重要です。 スレッドプールが大きすぎると、ほとんど待機モードになるスレッドを作成するだけで、不要なオーバーヘッドが発生します。 キュー内のタスクの待機時間が長いため、アプリケーションが応答しなくなったように見えることが少なすぎます。
Calling a Future‘s get() method after task cancellation:すでにキャンセルされたタスクの結果を取得しようとすると、CancellationException.がトリガーされます
Unexpectedly-long blocking with Future‘s get() method:タイムアウトは、予期しない待機を回避するために使用する必要があります。
この記事のコードはa GitHub repositoryで入手できます。