JPCERT コーディネーションセンター

TPS03-J. スレッドプールで実行されるタスクを通知なしに異常終了させない

スレッドプールで実行されるすべてのタスクは、異常終了したことをアプリケーションに通知する仕組みを提供しなければならない。スレッドプールではスレッドは再利用されるので、タスクの異常終了が通知されなくてもリソースリークが発生することはない。しかし、タスクの異常終了の原因究明が非常に困難になったり、不可能になったりする。

アプリケーションレベルで例外を処理する最もよい方法は、例外ハンドラを使うことである。診断動作、クリーンアップ処理、JVMの終了、あるいは障害情報の記録などを例外ハンドラで行うことができる。

違反コード (タスクの異常終了)

以下の違反コードは、スレッドプールをカプセル化した PoolService クラスおよび Runnable を実装する Task クラスから構成されている。Task.run() メソッドは NullPointerException のような実行時例外をスローする可能性がある。

final class PoolService {
  private final ExecutorService pool = Executors.newFixedThreadPool(10);

  public void doSomething() {
    pool.execute(new Task());
  }
}

final class Task implements Runnable {
  @Override public void run() {
    // ...
    throw new NullPointerException();
    // ...
  }
}

実行時例外が発生してタスクが異常終了する場合、タスクはアプリケーションに異常終了が起こったことを通知しない。また、タスクを復旧する仕組みもないため、TaskNullPointerException をスローしても、無視されることになる。

適合コード (ThreadPoolExecutor フック)

java.util.concurrent.ThreadPoolExecutor クラスの afterExecute() フックメソッドをオーバーライドすると、タスク固有の復旧動作やクリーンアップ動作を実行できる。このフックは、タスクが run() メソッドのすべての処理を実行して正常終了するか、例外が発生して停止した場合に呼び出される。JVMの実装によっては、java.lang.Error がキャッチされない場合がある。(詳細は、Bug ID 6450211 を参照[SDN 2008]。) このアプローチを実装するには、ExecutorServiceThreadPoolExecutor を拡張した自前のクラスに置き換え、afterExecute() フックメソッドを以下のようにオーバーライドして使用する。

final class PoolService {
 // 各引数値は、サンプルコードを見易くするために、ハードコーディングしている
  ExecutorService pool = new CustomThreadPoolExecutor(
      10, 10, 10, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(10));
  // ...
}

class CustomThreadPoolExecutor extends ThreadPoolExecutor {
  // ... コンストラクタ ...
  public CustomThreadPoolExecutor(
      int corePoolSize, int maximumPoolSize, long keepAliveTime, 
      TimeUnit unit, BlockingQueue<Runnable> workQueue) { 
    super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
  }


  @Override
  public void afterExecute(Runnable r, Throwable t) {
    super.afterExecute(r, t);
    if (t != null) {
      // 例外が発生したので、ハンドラへ転送
    }
    // ... タスク固有のクリーンアップ動作を実行
  }

  @Override
  public void terminated() {
    super.terminated();
  // ... クリーンアップの最終動作を実行
  }
}

terminated() フックメソッドは、すべてのタスクの実行が終了し、Executor が正常終了した後に呼び出される。このフックをオーバーライドして、(try ブロックの後始末を finally ブロックで行うのと同様に、)スレッドプールが獲得したリソースを解放することができる。

適合コード (Thread.UncaughtExceptionHandler インタフェース)

以下の適合コードでは、独自のキャッチされない例外ハンドラを設定している。スレッドプールの生成時に、ThreadFactory クラスのインスタンスを引数として渡しており、このファクトリオブジェクトが、新しいスレッドを作成するとともに、キャッチされない例外のハンドラをセットする。Task クラスには前述のコード例から変更はない。

final class PoolService {
  private static final ThreadFactory factory =
      new ExceptionThreadFactory(new MyExceptionHandler());
  private static final ExecutorService pool =
      Executors.newFixedThreadPool(10, factory);

  public void doSomething() {
    pool.execute(new Task()); // Task は Runnable を実装している
  }

  public static class ExceptionThreadFactory implements ThreadFactory  {
    private static final ThreadFactory defaultFactory =
        Executors.defaultThreadFactory();
    private final Thread.UncaughtExceptionHandler handler;

    public ExceptionThreadFactory(
        Thread.UncaughtExceptionHandler handler)
    {
      this.handler = handler;
    }

    @Override public Thread newThread(Runnable run) {
      Thread thread = defaultFactory.newThread(run);
      thread.setUncaughtExceptionHandler(handler);
      return thread;
    }
  }

  public static class MyExceptionHandler extends ExceptionReporter
      implements Thread.UncaughtExceptionHandler {
    // ...

    @Override public void uncaughtException(Thread thread, Throwable t) {
   // 復旧あるいはログ用のコードを記述
    }
  }
}

スレッドプールへのタスク割り当てに(execute() メソッドではなく) ExecutorService.submit() メソッドを使用することで、Future オブジェクトを得ることができる。ただし、ExecutorService.submit() メソッドを使う場合、スローされた例外は、キャッチされない例外用のハンドラではキャッチできないことに注意。これは、スローされた例外が、返り値のステータスの一部として ExecutionException クラスにラップされ、Future.get() メソッドにより再度スローされるからである [Goetz 2006a]。

適合コード (Future<V> クラスと submit() メソッド)

以下の適合コードでは、ExecutorService.submit() メソッドでタスクをサブミットし、Future オブジェクトを獲得している。Future オブジェクトを通じて例外をスローさせることで、タスクがスローした例外を PoolService クラスのなかで取り扱えるようにしている。

final class PoolService {
  private final ExecutorService pool = Executors.newFixedThreadPool(10);

  public void doSomething() {
    Future<?> future = pool.submit(new Task());

    // ...

    try {
      future.get();
    } catch (InterruptedException e) {
      Thread.currentThread().interrupt(); // 割込みステータスのリセット
    } catch (ExecutionException e) {
      Throwable exception = e.getCause();
   // 例外ハンドラへ転送
    }
  }
}

また、Future オブジェクト取得時に発生するあらゆる例外も、必要に応じて doSomething() メソッド内で処理できる。

例外

TPS03-EX0: Runnable あるいは Callable を実装するすべてのタスクのコードにおいて、例外が発生する可能性がないことが確認されているならば、このルールに適合していなくてもよい。ただし、復旧処理を行ったり、例外が発生したことを記録するために、タスク固有の、あるいはグローバルな、例外ハンドラを導入することは、一般的によい習慣である。

リスク評価

スレッドプール中のタスクが異常終了したことを報告する仕組みを提供しないと、問題の原因追求が困難になりうる。

ルール 深刻度 可能性 修正コスト 優先度 レベル
TPS03-J P4 L3
関連ガイドライン
MITRE CWE CWE-392. Missing report of error condition
参考文献
[API 2006] Interfaces ExecutorService, ThreadFactory; class Thread
[Goetz 2006a] Chapter 7.3, Handling Abnormal Thread Termination
翻訳元

これは以下のページを翻訳したものです。

TPS03-J. Ensure that tasks executing in a thread pool do not fail silently (revision 78)

Top へ

Topへ
最新情報(RSSメーリングリストTwitter