広告

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 が発生します。
しかし、今回も見た目上は特になにも起きず、ひっそりと完了してしまいました。

V call() throws Exception

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 をキャッチ

Futuregetメソッドは、チェック例外である ExecutionException を発生させます。

例外をスローすることによって中断したタスクの結果を取得しようとしたときにスローされる例外です。 この例外は、Throwable.getCause()メソッドを使用して検査できます。

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 できます。

protected void setException​(Throwable t)
このFutureが設定済みの場合または取り消された場合を除き、このFutureがExecutionExceptionと、その理由として指定されたスロー可能オブジェクトを報告するようになります。

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を使うこともおすすめです。
下記の記事にまとめていますので、そちらも参考にしていただけたら幸いです。


関連記事

ページの先頭へ