問題
複数のスレッドが平行してDBにアクセスするJavaプログラムを作成した。
ところが、スレッドが多くなるとDBMSで設定したDBコネクションの上限値を超えてしまい、プログラムがフリーズしてしまうことが分かった。
解決策
ExecutorServiceを使って、最大スレッド数を制限する。
ExecutorServiceとは何か
ExecutorServiceはJava標準の平行処理ライブラリ「java.util.concurrent」パッケージに含まれるインターフェースで、生のままでは使いにくいJavaのスレッドをラップし、使いやすくしてくれる。
■ ExecutorService
- 標準API
- Java1.5以上
- java.util.concurrentパッケージ
詳しくはJava8 API仕様 ExecutorServiceを参照。
テストプログラムの作成
スレッド
まずはただのスレッドを書く。
実際の処理は何もしないが、処理に時間がかかっているように見せかけるため、引数で与えた時間分、スリープしてから終了するようにする。
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スレッドとしておく。
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が正しく動作していることも、併せて確認できる。
サンプルコード
参考
Java8 API仕様 ExecutorService
ExecutorService の使い方
Thread.start()実行をやめよう。「ExecutorService」でスレッド管理が簡単に。
ExecutorServiceを使って、Javaでマルチスレッド処理
確認環境
Java 1.8