本日の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(); }
というのをやろうとしていたのでは? という見解がリーズナブルそうでした。