Executorフレームワークの使い方

Thread#start()ではなくExecutorフレームワークを使う。

Thread#start()ではなくExecutorフレームワークを使う。

Executorフレームワークとは

スレッドプールを利用した並列処理実行をするためのフレームワーク。利用者は「ExecutorService」インタフェースを意識するだけで、様々なポリシーを持つスレッドプールを統一して扱うことができる。

Executorフレームワーククラス図
Figure 1. Executorフレームワーククラス図

Executorフレームワークでは、並列処理を実行するのにスレッドプールを利用する。スレッドプールは処理をいったんキューに貯めておき、ポリシーに従い処理の実行開始を決定する。これは処理をいきなり開始してしまう方法に比べ、メモリ管理やスレッド再利用などの面から有効。プロデューサー・コンシューマパターンになっている。

スレッドプールのインスタンスは「Executors」クラスにファクトリが用意されているので、そこから生成する。それらは「ExecutorService」か「ScheduledExecutorService」を実装している。

Thread#start()では処理をいきなり開始するだけで、Executorフレームワークに比べて得がない。基本的によく設計されているこちらを使う。

基本的な使い方

Excutorフレームワークの基本的な使い方
Runnable command = new Runnable() {
  public void run() {
    System.out.println(Thread.currentThread().getName() + " 終了");
  }
};

ExecutorService executor = Executors.newFixedThreadPool(10);
try {
  executor.execute(command);
  executor.execute(command);
  executor.execute(command);
} finally {
  executor.shutdown();
  executor.awaitTermination(1, TimeUnit.MINUTES);
}
Table 1. ExecutorServiceの終了に関連するメソッド
メソッド 概要

shutdown

スレッドプールは新規タスクの受付を終了する。
shutdown()後に処理が渡されても例外となる。
受付済みや実行中のタスクについてはそのまま継続される。

shutdownNow

強制終了を開始する。
実行中のタスクについてはinterruptして、
未実行の受付済みタスクはリストにして戻り値とする。

awaitTermination

全タスクが終了するか指定時間が経過するまで処理をブロック(停止)する。

スレッドプールの生成とスレッドの実行

Executorsからスレッドプールを生成し、タスクをexecute() or submit()メソッドに渡せば実行される。タスクは「Runnable」or「Callable」で実装しておく。

Executorsには異なる実行ポリシーを持った多くのスレッドプールが定義されているので、好きなモノを選ぶ。実行は簡単なので、どうやって同期させたり終了させたりするのかに注力する。

スレッドプールをシャットダウンする

Executorフレームワークを使う上で忘れてはいけないのが、スレッドプールのシャットダウン。shutdown() or shutdownNow()によりスレッドプールはシャットダウン中・終了状態に向かうことができる。

スレッドプール暗黙状態遷移図
Figure 2. スレッドプールの暗黙的状態遷移図

シャットダウンをしていないということは、スレッドプールは新規のタスク受付を継続する。execute()で渡された処理が全て終了したとしてもタスクの受付は継続されるので、スレッドプールのタスクを受け付けるスレッドが生き残る現象が発生してしまう。全タスクが終了しても、タスクを受け付けるスレッドが生き残っているせいでJVMを終了できないことにつながる。

なのでスレッドプールを生成した場合は、必ずシャットダウンされることに細心の注意を払う。

穏やかな終了と唐突な終了

shutdown()とshutdownNow()については、受付済みや実行中タスクの扱い方が異なる。

shutdown()では穏やかな終了を試みる。shutdown()が呼び出されるとスレッドプールは新規タスクの受付を終了する。shutdown()後に処理が渡されても例外となる。受付済みや実行中のタスクについてはそのまま継続され、全タスクが自然終了したら終了状態になる。

残りのタスクがいつ終了するかはそのタスク次第なので、終了を待つ方法としてawaitTermination()がある。awaitTermination()は全タスクが終了するか指定時間が経過するまで処理をブロック(停止)する。それでもなお終了できないことを考慮する場合は、shutdown()後でもshutdownNow()を呼び出しも考える。

一方shutdownNow()は唐突な終了になり強制終了を開始する。実行中のタスクについてはinterruptして、未実行の受付済みタスクはリストにして戻り値とする。このとき、途中で実行終了したタスクを取得する方法が標準でないので、必要であれば記録されるように実装しておく。

可能であれば「穏やかな終了」に向かいたいが、タスクが想定時間内にしっかり終了できるかを保証はできないので「唐突な終了」もやむなしなこともある。なのでタスクの実装はinterruptされても再実行可能であったり実行失敗を記録したりできることも必要な場合がある。