Help us understand the problem. What is going on with this article?

JavaのExecutorServiceを使ってスレッド数を制限する

More than 1 year has passed since last update.

問題

複数のスレッドが平行してDBにアクセスするJavaプログラムを作成した。
ところが、スレッドが多くなるとDBMSで設定したDBコネクションの上限値を超えてしまい、プログラムがフリーズしてしまうことが分かった。

解決策

ExecutorServiceを使って、最大スレッド数を制限する。

ExecutorServiceとは何か

ExecutorServiceはJava標準の平行処理ライブラリ「java.util.concurrent」パッケージに含まれるインターフェースで、生のままでは使いにくいJavaのスレッドをラップし、使いやすくしてくれる。

■ ExecutorService

  • 標準API
  • Java1.5以上
  • java.util.concurrentパッケージ

詳しくはJava8 API仕様 ExecutorServiceを参照。

テストプログラムの作成

スレッド

まずはただのスレッドを書く。
実際の処理は何もしないが、処理に時間がかかっているように見せかけるため、引数で与えた時間分、スリープしてから終了するようにする。

SampleThread.java
package sample;

import java.text.SimpleDateFormat;
import java.util.Calendar;

public class SampleThread implements Runnable {

    private int no;
    private int time;

    SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");

    SampleThread(int no, int time) {
        this.no = no;
        this.time = time;
    }

    @Override
    public void run() {
        System.out.println(" No." + no + " start  id:" + Thread.currentThread().getId() + "  生存時間:" + time + " 現在時刻:" + sdf.format(Calendar.getInstance().getTime()));
        try {
            // 何かの処理が行われていると見せかけるため、スリープさせる。
            Thread.sleep(time * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(" No." + no + " end    id:" + Thread.currentThread().getId() + "  生存時間:" + time + " 現在時刻:" + sdf.format(Calendar.getInstance().getTime()));
    }
}

スレッドプールを持つExecutorService

次にExecutorServiceを使って、先ほど書いたスレッドを使ってみる。
ExecutorServiceはインターフェースなので、このインターフェースの実装が必要になるが、ここでExecutors#newFixedThreadPoolメソッドを使って、スレッドプールを持つExecutorServiceを手にいれる。
スレッドプールの上限値は、分かりやすさのために3スレッドとしておく。

Main.java
package sample;

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

public class Main {

    public static void main(String[] args) {

        // スレッドプールの最大値を3スレッドに設定。
        final int MAX_THREADS = 3;

        // ファクトリメソッド「newFixedThreadPool」を使って、スレッドプールを持つExecutorServiceを生成。
        ExecutorService executor = Executors.newFixedThreadPool(MAX_THREADS);

        // スレッドを10本立ててみる
        for (int i = 0; i < 10; i++) {

            // スレッドに番号を振る。
            int no = i;

            // スレッドの生存時間を1〜10秒でランダムに決める。
            int lifeTime = (int)(Math.random() * 9 + 1);

            // スレッドを起動する。
            executor.submit(new SampleThread(no, lifeTime));
        }
        // ExecutorServiceを閉じる。
        System.out.println("executor.shutdown();");
        executor.shutdown();
    }
}

テストの実施

上記のテストプログラムを流すと、以下がコンソール出力される。

項目      意味     
No. スレッドを起動するときに引数で与えた処理番号
id スレッド固有のid。
生存時間 スレッドがスリープしている時間。(秒)
コンソール
 No.0 start  id:12  生存時間:1 現在時刻:13:29:41
 No.1 start  id:13  生存時間:8 現在時刻:13:29:41
 No.2 start  id:14  生存時間:6 現在時刻:13:29:41
executor.shutdown();
 No.0 end    id:12  生存時間:1 現在時刻:13:29:42
 No.3 start  id:12  生存時間:1 現在時刻:13:29:42
 No.3 end    id:12  生存時間:1 現在時刻:13:29:43
 No.4 start  id:12  生存時間:9 現在時刻:13:29:43
 No.2 end    id:14  生存時間:6 現在時刻:13:29:47
 No.5 start  id:14  生存時間:2 現在時刻:13:29:47
 No.1 end    id:13  生存時間:8 現在時刻:13:29:49
 No.6 start  id:13  生存時間:9 現在時刻:13:29:49
 No.5 end    id:14  生存時間:2 現在時刻:13:29:49
 No.7 start  id:14  生存時間:7 現在時刻:13:29:49
 No.4 end    id:12  生存時間:9 現在時刻:13:29:52
 No.8 start  id:12  生存時間:7 現在時刻:13:29:52
 No.7 end    id:14  生存時間:7 現在時刻:13:29:56
 No.9 start  id:14  生存時間:4 現在時刻:13:29:56
 No.6 end    id:13  生存時間:9 現在時刻:13:29:58
 No.8 end    id:12  生存時間:7 現在時刻:13:29:59
 No.9 end    id:14  生存時間:4 現在時刻:13:30:00

テスト結果の分析

スレッドの最大数制限

「No.」の部分をみると、0〜9までのNo.があり、スレッドが10回処理されている。
一方、スレッドのidを見てみると、12〜14しかなく、スレッドの数は3に抑えられていることが分かる。
また、一つのスレッドの処理が終わると、同じidのスレッドが次の処理で使われていることも確認できる。
これらのことから、3本のスレッドが使い回されていると言える。
また、同時に実行されるスレッドの最大数が3であることも分かる。

ExecutorServiceの呼び出し元はブロックされない

スレッドの最大数の3本が起動された直後にexecutor#shutdownメソッドが呼ばれている。
これは、スレッドがスレッドプールの最大数に達しているときに、スレッドの起動元のsubmitがブロックされるのではなく、タスクそのものは全部FIFOのQueueで受け付けるためである。
このQueueが正しく動作していることも、併せて確認できる。

サンプルコード

https://github.com/nogitsune413/ExecutorServiceSample

参考

Java8 API仕様 ExecutorService
ExecutorService の使い方
Thread.start()実行をやめよう。「ExecutorService」でスレッド管理が簡単に。
ExecutorServiceを使って、Javaでマルチスレッド処理

確認環境

Java 1.8

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away