Java のExecutorService での処理のキャンセル方法

通常わざわざスレッドを分けて行うタスクは、その処理に時間がかかるものです。 (逆に言えばそもそも、即座に処理が終わるようなタスクをわざわざスレッドを分けて行うのは適切ではありません。)

時間のかかる処理となれば、途中で処理を中断したい場合もあるでしょう。

処理をキャンセルしたい場合、どうしたらよいでしょうか。

ここでは ExecutorService での処理のキャンセル方法について説明します。

ExecutorService での処理のキャンセル

ExecutorService の使い方 で説明したように Executor によって処理されるタスクの状態は次の四つです。

ExecutorService タスクの状態

タスクは submit()メソッドまたは execute() メソッドでサブミットされます。

スレッドプール内のワーカースレッドが全てビジー (他のタスクの処理をしている状態) であれば、サブミットしても処理は直ちには開始されず、タスクはブロッキングキューに submitted 状態のまま入ります。

さて、ExecutorService の submit() メソッドでタスクをサブミットすると、その戻り値として Future オブジェクトが返ります。

Future オブジェクトは非同期処理の結果を表します。

Future オブジェクトの cancel() メソッドを呼ぶことで、タスクの処理のキャンセル要求をすることができます。

まだ開始していないタスク、つまり submitted 状態のタスクであれば、 started 状態にならずに直ちにキューから削除されます。

既に開始しているタスク (started 状態) の振る舞いは、 cancel() メソッドに渡すフラグで変わります。cancel() メソッドの第一引数は割込みフラグです。

割込みフラグを false にして、 cancel() メソッドを呼んだ場合は、 started 状態のタスクはキャンセルされずにそのまま続行されます。

一方、 割込みフラグを true にして、 cancel() メソッドを呼んだ場合、そのタスクを実行しているスレッドが 「interrupted 状態」になります。

タスクの run メソッドの中で Thread.interrupted() メソッドを呼ぶことで、実行中のスレッドが interrupted 状態になっていないかチェックできます。 これによって、処理を中断することが要求されたことがわかります。

中断のために必要な処理があればそれを実行するなり、あるいは特に何もなければ InterruptedException を投げて処理を中断するなどします。

サブミットしたタスクの処理を中断するサンプルコード

まず、Runnable インターフェイスを実装した簡単なタスクを用意します。

package com.keicode.java.test;

public class MyTask implements Runnable {
  String name;

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

  @Override
  public void run() {
    System.out.printf("[%s] Start\n", this.name);

    try {
      Thread.sleep(3000);
    } catch (InterruptedException e) {
      System.out.printf("[%s*] Interrupted\n", this.name);
      return;
    }

    System.out.printf("[%s] End\n", this.name);
  }
}

コンストラクタで任意の名前を受け取り、 run() メソッドの実行を開始したら、 [名前] Start と出力します。 3秒スリープして、 run() を抜ける時に [名前] End と出力します。

sleep() でスリープしている時にスレッドがインターラプトされたら、InterruptedException が発生します。InterruptedException が発生したら [名前*] Interrupted と出力して終了します。

このタスクを、スレッドをひとつだけのスレッドプールを作成して、そこに二つサブミットします。

サブミットしたら、(念のため100ミリ秒だけ待ってから) すぐに二つとも、割込みフラグを true にしてキャンセルします。

package com.keicode.java.test;

import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class TestApp {

  public static void main(String[] args) throws InterruptedException {
    Random random = new Random();
    ExecutorService pool = Executors.newFixedThreadPool(1);

    Future future1 = pool.submit(new MyTask("1"));
    Future future2 = pool.submit(new MyTask("2"));

    Thread.sleep(100);

    future1.cancel(true);
    future2.cancel(true);

    pool.shutdown();
  }
}

この実行結果は次のようになります。

[1] Start
[1*] Interrupted

ひとつめのタスクは実行中だったため、確かにインターラプトされて中断しています。二つ目のタスクはキューに入ったままで、 まだ実行中ではなかったため、全く処理が行われなかったことがわかります。

次に割込みフラグを false にしてキャンセルします。(上の19行目と20行目で false を渡すだけの違いなのでコードの掲載は省略します)

この実行結果は次の通りです。

[1] Start
[1] End

ひとつ目の処理はインターラプトされることなく、処理されています。二つ目の処理はやはり全く処理されていません。

以上のように、 cancel() を呼んでも直ちに処理が全て中断されるわけではなく、 スレッドの状態が interrupted 状態になるだけです。

このため、時間のかかる処理の中断を実装する時には、時々 Thread.interrupted() メソッドを呼び、処理を中断すべきかチェックする必要があります。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Java 入門