Java の ExecutorService の使い方

Java の ExecutorService でスレッドプールを利用できる

ここで説明するのは、おそらくもっとも安全にマルチスレッドプログラムを書く方法です。

さらに同様の方法で簡単に拡張することで、複数のスレッドを効率よく使うスレッドプール (Thread Pool) を利用できますので、 ぜひ覚えておきたい方法です。

その方法とは、 ExecutorService を利用することです。

ExecutorService では、 「Java のマルチスレッド・プログラミングの基本」 でみたように Thread オブジェクトをそのまま生では使いません。

ExecutorService ではスレッドプールが用意されるので、そこにタスクがディスパッチされることで処理が行われます。

Java のタスクの状態

Executor によって処理されるタスクの状態は次の四つです。

ExecutorService タスクの状態

  1. created (作成された) : サブミットされていない状態のタスクは created ステートです。
  2. submitted (サブミットされた) : submit または execute メソッドでタスクをサブミットします。サブミットされても処理が開始しない場合は、通常ブロッキングキューに入ります。 FIFO で処理されます。 submit された以降、メモリが割り当てられない等何らかの理由で処理がアボートする場合は、RejectedExecutionException が発生します。
  3. started (処理が開始した)
  4. complted (完了した)

この状態は不可逆で、 created から completed へと順番に変わります。

スレッドプールとは?

さて、スレッドプール (Thread Pool) について簡単に説明します。

スレッドプール

スレッドプールは通常 Web サーバーやデータベースサーバーなど、複数のタスクを同時に素早く処理しなければならない状況で利用されます。

スレッドプールというのは、複数のスレッドをあらかじめ作成して待機させておき、タスクが来たら待っているスレッドにタスクを割り当てて処理を開始させる、という仕組みのことをいいます。

ゼロからこうした仕組みを実装するとなると、少々面倒くさいのですが、 Java では ExecutorService を使うことでスレッドプールを簡単に利用できます。

ExecutorService のサンプルコード

では、実際にスレッドプールを用いてタスクの処理を行う簡単な例を示します。

まずはタスクです。タスクは Runnable インターフェイスを実装したクラスとして実装します。

ここでは次のように、開始と終了時に動作しているスレッド名や割り当てたタスクの番号を出力するだけにしています。 途中でランダムな値 (ミリ秒) だけスリープしています。

package com.keicode.java.test;

public class SimpleTask implements Runnable {
  int number;
  int wait;

  public SimpleTask(int number, int wait) {
    this.number = number;
    this.wait = wait;
  }

  public void run() {
    String threadName = Thread.currentThread().getName();
    System.out.printf(
        "+++ Begin [%d] on %s +++\n",
        number, threadName);
    try {
      Thread.sleep(wait);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    System.out.printf(
        "+++ End [%d] on %s +++\n",
        number, threadName);
  }
}

このタスクを、次のようにして ExecutionService を使って、処理します。

package com.keicode.java.test;

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

public class TestApp {
  public static void main(String[] args) {
    Random random = new Random();
    String threadName = Thread.currentThread().getName();
    System.out.printf("+++ Begin %s +++\n", threadName);

    // スレッドプールの作成
    ExecutorService pool = Executors.newFixedThreadPool(3);

    for (int i = 0; i < 10; i++) {
      System.out.printf("* Submit [%d]\n", i);
      pool.submit(new SimpleTask(i, random.nextInt(1000)));
    }

    // シャットダウン
    pool.shutdown();

    System.out.printf("+++ End %s +++\n", threadName);
  }
}

実行結果は次のようになりました。

+++ Begin main +++
* Submit [0]
* Submit [1]
+++ Begin [0] on pool-1-thread-1 +++
* Submit [2]
+++ Begin [1] on pool-1-thread-2 +++
+++ Begin [2] on pool-1-thread-3 +++
* Submit [3]
* Submit [4]
* Submit [5]
* Submit [6]
* Submit [7]
* Submit [8]
* Submit [9]
+++ End main +++
+++ End [0] on pool-1-thread-1 +++
+++ Begin [3] on pool-1-thread-1 +++
+++ End [2] on pool-1-thread-3 +++
+++ Begin [4] on pool-1-thread-3 +++
+++ End [4] on pool-1-thread-3 +++
+++ Begin [5] on pool-1-thread-3 +++
+++ End [5] on pool-1-thread-3 +++
+++ Begin [6] on pool-1-thread-3 +++
+++ End [1] on pool-1-thread-2 +++
+++ Begin [7] on pool-1-thread-2 +++
+++ End [3] on pool-1-thread-1 +++
+++ Begin [8] on pool-1-thread-1 +++
+++ End [8] on pool-1-thread-1 +++
+++ Begin [9] on pool-1-thread-1 +++
+++ End [6] on pool-1-thread-3 +++
+++ End [7] on pool-1-thread-2 +++
+++ End [9] on pool-1-thread-1 +++

ここではスレッドプール内に 3 つのスレッドを作成して、そこに 10 個のタスクを投げています。それぞれのタスクは、最大 1 秒 (1000ミリ秒) のランダムな時間だけ待機してから終了メッセージを出力して終了します。

出力結果から、 スレッド名が pool-1-thread-1 から pool-1-thread-3 までの三つのスレッドが実行していたことがわかります。

java.util.concurrent.Executors.newFixedThreadPool というメソッドで、固定数のスレッドを持つスレッドプールを利用する ExecutorService を取得します。

ExecutorService の submit() メソッドにタスクを渡すと、タスクは submitted 状態になります。

ここでは一度に 10 個のタスクの実行を要求していますが、スレッドは 3 個だけです。このためまず 3 個のタスクが started になり、 残りの 7 個は submitted ステートのままキューの中で待ちます。

開始したタスクが終了したら、キューの中のタスクがスレッドに割り当てられて started 状態になります。

ExecutorService の shutdown() メソッドの動作

スレッドプールの shutdown() メソッドを呼ぶと、新しいタスクをサブミットできなくなります。

全てのタスクの処理が終了したら、スレッドプールが終了します。

逆に shutdown() を呼ばないと、スレッドプールはタスクを待ち続けるので、スレッドプールが待機したままになります。

このため、メインスレッドが終了してもプログラムは実行したままになります。

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

© 2024 Java 入門