Java : スレッドで発生した例外は忘れずに処理しよう
スレッドで発生した例外は、適切に処理しないと握りつぶされてしまうことがあります。
そうすると、いざ例外が発生したときに、問題解析に苦労することになるでしょう。
本記事では、スレッドで発生した例外の処理方法について、いくつかご紹介します。
対象読者 : ExecutorService をある程度使ったことのあるかた
例外が握りつぶされてしまう例
さっそくコード例を見ていきましょう。
final var executorService = Executors.newWorkStealingPool(1);
try {
final List<Runnable> runnableList = List.of(
() -> {
System.out.println("abcd");
},
() -> {
System.out.println("XYZ");
},
() -> {
throw new RuntimeException("例外発生!");
}
);
for (final var runnable : runnableList) {
executorService.submit(runnable);
}
} finally {
executorService.shutdown();
}
final var term = executorService.awaitTermination(10, TimeUnit.SECONDS);
System.out.println("term : " + term);
// 結果
// ↓
//abcd
//XYZ
//term : true
上記のコードでは、3つのタスク (Runnable) を、ExecutorService に submit します。
タスクは、それぞれ
- "abcd"を出力
- "XYZ"を出力
- 非チェック例外を発生
という処理を行います。
1、2番目のタスクは正常に完了して、"abcd", "XYZ" をコンソールに出力しました。
3番目の処理は、見た目上は特になにも起きず、ひっそりと完了してしまいました。
今回は単純な例なので問題解析は楽かもしれません。
しかし、この手の問題はしばらくしてから発覚することが多い印象です。
あれ、処理されていないタスクがあるっぽい?
↓
コンソールやログファイルにはなにも出力されていない…
↓
解析難航
なんてことも…
もう1つ例を見てみましょう。
今度は Callable を使う例です。
まず読み込むためのファイルを準備します。
以下はWindows 10のPowerShellでファイルを確認した手順です。
// --- PowerShell ---
PS R:\java-work> ls -name
aaa.txt
bbb.txt
PS R:\java-work> cat .\aaa.txt
abcd
PS R:\java-work> cat .\bbb.txt
XYZ
aaa.txtの内容は"abcd"、bbb.txtは"XYZ"です。
ccc.txtはファイル自体がありません。
final var executorService = Executors.newWorkStealingPool(1);
try {
final List<Callable<Void>> callableList = List.of(
() -> {
final var path = Path.of("R:", "java-work", "aaa.txt");
final var str = Files.readString(path);
System.out.println(str);
return null;
},
() -> {
final var path = Path.of("R:", "java-work", "bbb.txt");
final var str = Files.readString(path);
System.out.println(str);
return null;
},
() -> {
final var path = Path.of("R:", "java-work", "ccc.txt");
final var str = Files.readString(path);
System.out.println(str);
return null;
}
);
for (final var callable : callableList) {
executorService.submit(callable);
}
} finally {
executorService.shutdown();
}
final var term = executorService.awaitTermination(10, TimeUnit.SECONDS);
System.out.println("term : " + term);
// 結果
// ↓
//abcd
//XYZ
//term : true
3番目のタスクでは、ccc.txt が見つからないのでチェック例外の IOException が発生します。
しかし、今回も見た目上は特になにも起きず、ひっそりと完了してしまいました。
Callable の callメソッドは throws Exception と定義されているので、すべての例外を通知してしまいます。
そのため、チェック例外でさえキャッチし忘れる可能性があります。
例外を処理する方法
3つの方法をご紹介します。
おすすめなのは3番目の FutureTask を使う方法です。
スレッドの根元でキャッチ
もっとも単純な方法です。
規模の小さなプログラムでは有効です。
public class SampleRunnable implements Runnable {
private final Path path;
public SampleRunnable(Path path) {
this.path = path;
}
@Override
public void run() {
try {
final var str = Files.readString(path);
System.out.println(str);
} catch (Throwable t) {
System.out.println(t);
}
}
}
run メソッドで処理する内容をすべて try で囲みます。
そして発生するすべての例外をキャッチし、問題解析のための情報を出力します。
注意
- catch (Throwable t) を使うのは、今回のようなスレッド処理をまるごとtryする用途のときだけにしましょう。
- 通常処理の try/catch では、catch(IOException e) のようにして、Error や RuntimeException はキャッチしないようにしましょう。
final var executorService = Executors.newWorkStealingPool(1);
try {
final List<Runnable> runnableList = List.of(
new SampleRunnable(Path.of("R:", "java-work", "aaa.txt")),
new SampleRunnable(Path.of("R:", "java-work", "bbb.txt")),
new SampleRunnable(Path.of("R:", "java-work", "ccc.txt"))
);
for (final var runnable : runnableList) {
executorService.submit(runnable);
}
} finally {
executorService.shutdown();
}
final var term = executorService.awaitTermination(10, TimeUnit.SECONDS);
System.out.println("term : " + term);
// 結果
// ↓
//abcd
//XYZ
//java.nio.file.NoSuchFileException: R:\java-work\ccc.txt
//term : true
今回は、3番目のタスクで例外が発生したことがわかるようになりました。
これで問題解析も各段とやりやすくなります。
Future.get で ExecutionException をキャッチ
Futureの getメソッドは、チェック例外である ExecutionException を発生させます。
final var executorService = Executors.newWorkStealingPool(1);
try {
final List<Callable<Void>> callableList = List.of(
() -> {
final var path = Path.of("R:", "java-work", "aaa.txt");
final var str = Files.readString(path);
System.out.println(str);
return null;
},
() -> {
final var path = Path.of("R:", "java-work", "bbb.txt");
final var str = Files.readString(path);
System.out.println(str);
return null;
},
() -> {
final var path = Path.of("R:", "java-work", "ccc.txt");
final var str = Files.readString(path);
System.out.println(str);
return null;
}
);
final var futures = new ArrayList<Future<Void>>();
for (final var callable : callableList) {
final var f = executorService.submit(callable);
futures.add(f);
}
for (final var future : futures) {
try {
future.get();
} catch (ExecutionException e) {
System.out.println("ExecutionException! : " + e.getCause());
}
}
} finally {
executorService.shutdown();
}
final var term = executorService.awaitTermination(10, TimeUnit.SECONDS);
System.out.println("term : " + term);
// 結果
// ↓
//abcd
//XYZ
//ExecutionException! : java.lang.RuntimeException: java.lang.RuntimeException: java.nio.file.NoSuchFileException: R:\java-work\ccc.txt
//term : true
3番目のタスクで例外が発生したことがわかるようになりました。
ただ、RuntimeException が2つも連なってますね…
でもまぁ許容範囲でしょう。
あと、今回は Callable<Void> なので get をし忘れる可能性はあります。
Void以外なら、getを忘れることもないのでありだとは思います。
FutureTask.setExceptionをオーバーライド
FutureTask には、タスクの後処理に便利なオーバーライド可能なメソッドがいくつかあります。
また、FutureTask は Runnable でもあるので、そのまま ExecutorService に submit できます。
setException メソッドは、タスクを中断させた例外があるときに呼び出されます。
オーバーライドして使います。
public class SampleFutureTask<T> extends FutureTask<T> {
public SampleFutureTask(Callable<T> callable) {
super(callable);
}
@Override
protected void setException(Throwable t) {
super.setException(t);
System.out.println("setException : " + t);
}
}
final var executorService = Executors.newWorkStealingPool(1);
try {
final List<Runnable> tasks = List.of(
new SampleFutureTask<Void>(() -> {
final var path = Path.of("R:", "java-work", "aaa.txt");
final var str = Files.readString(path);
System.out.println(str);
return null;
}),
new SampleFutureTask<Void>(() -> {
final var path = Path.of("R:", "java-work", "bbb.txt");
final var str = Files.readString(path);
System.out.println(str);
return null;
}),
new SampleFutureTask<Void>(() -> {
final var path = Path.of("R:", "java-work", "ccc.txt");
final var str = Files.readString(path);
System.out.println(str);
return null;
})
);
for (final var task : tasks) {
executorService.submit(task);
}
} finally {
executorService.shutdown();
}
final var term = executorService.awaitTermination(10, TimeUnit.SECONDS);
System.out.println("term : " + term);
// 結果
// ↓
//abcd
//XYZ
//setException : java.nio.file.NoSuchFileException: R:\java-work\ccc.txt
//term : true
無事に例外が出力されるようになりました。
個人的におすすめの方法です。
まとめ
スレッドに関連する不具合は、本当に難解になることが多いです。
そのため、問題解析がしやすくなるように、普段から気にかけてコーディングすることが重要です。
Javaの例外は、スタックトレースも分かるし、問題解析に非常に有用な情報です。
ぜひ、ログとして情報を出力するようにしましょう。
ある程度大きなプログラムではロギングAPIを使うこともおすすめです。
下記の記事にまとめていますので、そちらも参考にしていただけたら幸いです。
関連記事
- API 使用例
- BlockingQueue (ブロッキング・キュー)
- Callable
- CancellationException
- ConcurrentHashMap.KeySetView (並列処理用セット)
- ConcurrentLinkedDeque (並列処理用・両端キュー)
- ConcurrentLinkedQueue (並列処理用キュー)
- ConcurrentMap (並列処理用マップ)
- ConcurrentModificationException (並列処理例外)
- ConcurrentSkipListSet (並列処理用セット)
- Condition (同期)
- CopyOnWriteArrayList (並列処理用リスト)
- CopyOnWriteArraySet (並列処理用セット)
- CountDownLatch (同期)
- CyclicBarrier (同期)
- Exchanger (同期)
- Executor
- ExecutorService
- Executors
- Future
- Future.State
- FutureTask
- InterruptedException (割込み例外)
- Lock (同期)
- Object (オブジェクト)
- Runnable
- Semaphore (セマフォ)
- Thread (スレッド)
- ThreadGroup
- ThreadLocal
- TimeUnit