torutkのブログ

ソフトウェア・エンジニアのブログ

Executors.newSingleThreadExecutor()が返すもの

本日のJava読書会「JUnit実践入門」において、次のサンプルコードからスレッドリークの有無が話題になりました。

  public void invoke() {
    Executors.newSingleThreadExecutor().execute(task);
  }

Executorはスレッドを内部に抱えるので、invoke()が何回も呼ばれ、shutdown()をしてあげてないこのコードではスレッドリークが生じるのでは?ということでした。

ここで生成されるExecutorインスタンスはメソッドスコープなので、すぐにGCされる対象となるのでは? という意見もありましたが、shutdownしないとスレッドが保持されたままでGCされないのでは、ということのようです。

で、jdk 7のライブラリのソースコードjdkインストールディレクトリのsrc.zip)を調べてみました。

まず、ExecutorsクラスのソースファイルExecutors.javaからメソッドnewSingleThreadExecutorの定義箇所を探しました。

  public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService(
      new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
                             new LinkedBlockingQueue<Runnable>()));
  }

と、スレッドプール数1のスレッドプールを生成し、それをFinalizableDelegatedExecutorServiceがラップしています。

FinalizableDelegatedExecutorServiceクラスは、同じExecutorsクラスのネストクラスになっていて、Executors.javaの中に定義があります。

    static class FinalizableDelegatedExecutorService
        extends DelegatedExecutorService {
        FinalizableDelegatedExecutorService(ExecutorService executor) {
            super(executor);
        }
        protected void finalize() {
            super.shutdown();
        }
    }

このスーパークラスのDelegatedExecutorServiceは、ExecutorServiceインスタンスをフィールドにもち、AbstractExecutorServiceクラスのメソッドをフィールドに持つExecutorServiceに転送する実装をしているクラスです。

つまるところ、最初のコードで生成されるメソッドスコープのインスタンス(FinalizableDelegatedExecutorServiceクラス)は、メソッドスコープを抜けたときにGC対象となり、GCされるとfinalizeメソッドでshutdownを呼ぶ仕掛けとなっています。

shutdownを呼ぶと、以後新しい実行要求は受け付けなくなり、現在実行中のタスクと実行キューにあるタスクを実行してから終了します。

このため、「JUnit実践入門」にあるコードはfinalizeによる安全ネットで計算機リソースの一つであるスレッドをリークせずに(GCが動く限り)済ますことができます。

ただし、そのコードはnewSingleThreadExecutor()を使っていますが、生成されたExecutorServiceインスタンスを保持していないので、タスクは並行に実行され得ることとなります。

Executorは1回生成したらフィールドに保持して使いまわすのが基本と思っているので、最初このコードの意図が分からず読解に苦労しました。

これは、JSR 166(java.util.concurrent)が導入される前のコードで書くと

  public void invoke() {
    new Thread(task).start();
  }

というのをやろうとしていたのでは? という見解がリーズナブルそうでした。