1. 概要

ExecutorService は、非同期モードでのタスクの実行を簡素化するJDKAPIです。 一般的に、 ExecutorService は、スレッドのプールとそれにタスクを割り当てるためのAPIを自動的に提供します。

2. ExecutorServiceのインスタンス化

2.1. Executorsクラスのファクトリメソッド

ExecutorService を作成する最も簡単な方法は、Executorsクラスのファクトリメソッドの1つを使用することです。

たとえば、次のコード行は、10個のスレッドを持つスレッドプールを作成します。

ExecutorService executor = Executors.newFixedThreadPool(10);

特定のユースケースを満たす定義済みExecutorServiceを作成するファクトリメソッドは他にもいくつかあります。 ニーズに最適な方法を見つけるには、Oracleの公式ドキュメントを参照してください。

2.2. ExecutorServiceを直接作成します

ExecutorService はインターフェースであるため、その実装のインスタンスを使用できます。 java.util.concurrent パッケージにはいくつかの実装から選択できますが、独自の実装を作成することもできます。

たとえば、 ThreadPoolExecutor クラスには、エグゼキューターサービスとその内部プールを構成するために使用できるコンストラクターがいくつかあります。

ExecutorService executorService = 
  new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,   
  new LinkedBlockingQueue<Runnable>());

上記のコードは、 ソースコードファクトリメソッドの newSingleThreadExecutor()。 ほとんどの場合、詳細な手動構成は必要ありません。

3. 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);

Execute(X145X]インターフェイスから継承される execute()や、 submit()[ X182X]、 invokeAny()および invokeAll()

execute()メソッドは void であり、タスクの実行結果を取得したり、タスクのステータス(実行中かどうか)を確認したりすることはできません。

executorService.execute(runnableTask);

submit()は、CallableまたはRunnableタスクを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);

先に進む前に、 ExecutorService のシャットダウンと、Futureのリターンタイプの処理という2つの項目について説明する必要があります。

4. ExecutorServiceをシャットダウンします

一般に、処理するタスクがない場合、ExecutorServiceは自動的に破棄されません。 それは生き続け、新しい仕事が行われるのを待ちます。

これは、アプリが不規則に表示されるタスクを処理する必要がある場合や、コンパイル時にタスクの量がわからない場合など、非常に役立つ場合があります。

一方、 ExecutorService を待機すると、JVMが実行を継続するため、アプリは終了する可能性がありますが、停止することはできません。

ExecutorService を適切にシャットダウンするために、 shutdown()および shutdownNow()APIがあります。

shutdown()メソッドは、ExecutorServiceの即時破棄を引き起こしません。 これにより、 ExecutorService は新しいタスクの受け入れを停止し、実行中のすべてのスレッドが現在の作業を終了した後にシャットダウンします。

executorService.shutdown();

shutdownNow()メソッドは、 ExecutorService をすぐに破棄しようとしますが、実行中のすべてのスレッドが同時に停止することを保証するものではありません。

List<Runnable> notExecutedTasks = executorService.shutDownNow();

このメソッドは、処理を待機しているタスクのリストを返します。 これらのタスクをどうするかを決めるのは開発者次第です。

ExecutorService (Oracle でも推奨)をシャットダウンする良い方法の1つは、これらのメソッドを両方とも awaitTermination()メソッドと組み合わせて使用することです。

executorService.shutdown();
try {
    if (!executorService.awaitTermination(800, TimeUnit.MILLISECONDS)) {
        executorService.shutdownNow();
    } 
} catch (InterruptedException e) {
    executorService.shutdownNow();
}

このアプローチでは、 ExecutorService は最初に新しいタスクの実行を停止し、次にすべてのタスクが完了するまで指定された期間待機します。 その時間が経過すると、実行はただちに停止されます。

5. Futureインターフェース

submit()および invokeAll()メソッドは、 Future タイプのオブジェクトまたはオブジェクトのコレクションを返します。これにより、タスクの実行結果を取得できます。または、タスクのステータスを確認します(実行中ですか)。

Future インターフェースは、特別なブロッキングメソッド get()を提供します。これは、Callableタスクの実行または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();

6. ScheduledExecutorServiceインターフェース

ScheduledExecutorService は、事前定義された遅延の後、および/または定期的にタスクを実行します。

繰り返しになりますが、 ScheduledExecutorService をインスタンス化する最良の方法は、Executorsクラスのファクトリメソッドを使用することです。

このセクションでは、ScheduledExecutorServiceを1つのスレッドで使用します。

ScheduledExecutorService executorService = Executors
  .newSingleThreadScheduledExecutor();

一定の遅延後に単一のタスクの実行をスケジュールするには、 ScheduledExecutorServicescheduled()メソッドを使用します。

2つのscheduled()メソッドを使用すると、RunnableまたはCallableタスクを実行できます。

Future<String> resultFuture = 
  executorService.schedule(callableTask, 1, TimeUnit.SECONDS);

scheduleAtFixedRate()メソッドを使用すると、一定の遅延後に定期的にタスクを実行できます。 上記のコードは、callableTaskを実行する前に1秒間遅延します。

次のコードブロックは、100ミリ秒の初期遅延後にタスクを実行します。 その後、450ミリ秒ごとに同じタスクを実行します。

Future<String> resultFuture = service
  .scheduleAtFixedRate(runnableTask, 100, 450, TimeUnit.MILLISECONDS);

プロセッサが割り当てられたタスクを実行するのにscheduleAtFixedRate()メソッドの period パラメータよりも長い時間が必要な場合、ScheduledExecutorServiceは現在のタスクが完了するまで待機します次を始める前に。

タスクの反復間で固定長の遅延が必要な場合は、 scheduleWithFixedDelay()を使用する必要があります。

たとえば、次のコードは、現在の実行の終了から別の実行の開始までの間に150ミリ秒の一時停止を保証します。

service.scheduleWithFixedDelay(task, 100, 150, TimeUnit.MILLISECONDS);

scheduleAtFixedRate()および scheduleWithFixedDelay()メソッドコントラクトによると、タスクの期間実行は、 ExecutorService の終了時、または例外がスローされた場合に終了します。タスク実行中

7. ExecutorService vs Fork / Join

Java 7のリリース後、多くの開発者はExecutorServiceフレームワークをfork/joinフレームワークに置き換えることを決定しました。

ただし、これは必ずしも正しい決定ではありません。 フォーク/結合に関連する単純さと頻繁なパフォーマンスの向上にもかかわらず、同時実行に対する開発者の制御が低下します。

ExecutorService を使用すると、開発者は、生成されるスレッドの数と、個別のスレッドで実行する必要のあるタスクの粒度を制御できます。 ExecutorService の最適な使用例は、「1つのタスクに対して1つのスレッド」というスキームに従ったトランザクションやリクエストなどの独立したタスクの処理です。

対照的に、 Oracleのドキュメントによると、fork / joinは、再帰的に小さな断片に分割できる作業を高速化するように設計されています。

8. 結論

ExecutorService は比較的単純ですが、いくつかの一般的な落とし穴があります。

それらを要約しましょう:

未使用のExecutorServiceを存続させる ExecutorService をシャットダウンする方法については、セクション4の詳細な説明を参照してください。

固定長のスレッドプールを使用しているときの間違ったスレッドプール容量:アプリケーションがタスクを効率的に実行するために必要なスレッドの数を決定することは非常に重要です。 スレッドプールが大きすぎると、ほとんどが待機モードになるスレッドを作成するためだけに不要なオーバーヘッドが発生します。 キュー内のタスクの待機時間が長いため、少なすぎるとアプリケーションが応答しなくなったように見える可能性があります。

タスクのキャンセル後にFutureのget()メソッドを呼び出す:すでにキャンセルされたタスクの結果を取得しようとすると、CancellationExceptionがトリガーされます。

Futureのget()メソッドによる予期しない長いブロッキング:予期しない待機を回避するためにタイムアウトを使用する必要があります。

いつものように、この記事のコードはGitHubリポジトリで入手できます。