JavaアプリケーションサーバでThreadLocal利用時の注意点
本日はJava EE Advent CalendarとJPOUG Advent Calndarの14日目です*1。
さて、先日11/9のJJUG CCCで話してきた内容で、Javaアプリケーションサーバでは、アプリケーションからThreadLocalは極力利用しない方がよいとのスライドを載せていました。
しかし、当日は時間がなく、また参考情報程度でしたので、説明を省いていました。
これについて、このエントリでは少し丁寧に説明をしたいと思います。
実は、上記スライドは、当日話した内容から以下の修正を加えています。
- 「極力利用しない」→「注意して利用」に変更
- 「再利用前提のスレッドに紐づくため、アプリで明示的に破棄」を追記
これがサマリにはなるのですが、説明していきましょう。
ThreadLocal
まず、ThreadLocalについて馴染みが薄い方もいると思いますので、簡単に復習しましょう。
ThreadLocalは、Javaでスレッドごとにオブジェクトを保持するための機構であり、スレッドセーフにデータの利用を行える便利なクラスです。
イメージ的には、カレントスレッドをキーに値の出し入れを行えるMapのようなものです*2。
などが参考になります。
アプリケーションで直接的に利用しないでも、フレームワークや、3rd Party製のライブラリなどで使われているケースもあります。
例えば有名なロギングライブラリであるLog4jやLogbackではMDC(Mapped Diagnostic Context)を実装するために、ThreadLocalが利用されています。
再利用前提のスレッド
Javaアプリケーションサーバでは、複数ユーザーからの同時アクセスを効率的に捌くために、リクエストごとにスレッドの生成や破棄を行うのではなく、スレッドプールを作成しておき、再利用するのが一般的です。
実装にもよりますが、アプリケーションサーバはそれぞれ一つのJavaプロセスとなり、一つ以上のアプリケーション(warやear)がデプロイされて動作することが多いです。
そのため、スレッドプールも複数のアプリケーションで共用されるリソースとなり、アプリケーションをアンデプロイしても、スレッドプールはそのまま残る実装が多いと思います。
もちろん、実際の運用では、アプリケーション間の競合を避けたり、独立性を保つために、アプリケーションごとにリソースを用意するケースもあるとは思いますが、基本的にはアプリケーションと、アプリケーションサーバのリソースは、できるだけ疎に保っておくような配慮が必要とされます。
これはスレッドプールだけではなく、DBとの接続プールなども同様です。
上記の組み合わせ
さて、「ThreadLocal」と「再利用前提のスレッド」を組み合わせたときにどうなるか。
何も考えていないと、アプリケーションサーバからアプリケーションをアンデプロイされても、スレッドは残ったままであり、ThreadLocalへの参照は破棄されません。
そのため、参照されているオブジェクトやクラスがGC対象とならず、残ってしまうことになりますので、メモリリークを発生させる要因となります。
以下の資料P22-P42で分かりやすく説明されていますので、図などを含めて参照してみてください。
ここでは、WebLogic Serverの本番再デプロイメントを繰り返し行うことにより、発生したOutOfMemoryErrorをJRockit Mission Controlに含まれるMemory Leak Detector*3を利用して特定・解決した例ですが、それに限らず、他製品での通常のアンデプロイや他のJVMでも発生するものです。
解決策
ThreadLocalを使わなければ原理的に発生はしないのですが、利用したい場合も多々発生しますし、3rd Partyのライブラリで暗黙的に利用されているケースもあります。
いくつか方法を提示しますが、実際には、デプロイ→アプリケーションの実行→アンデプロイを繰り返しテストして、メモリリークが発生しないよう確認ください。
リクエストごとに確実にThreadLocal#removeする
アプリケーションから直接ThreadLocalをハンドリングしている場合は、これを行うべきです。
リクエスト単位に値を初期化する使い方をする場合は、finallyやtry-with-resourcesでラッパーして、その中でThreadLocalの値を破棄するようにしましょう。
そうしなければ、再デプロイに限らず、他のリクエストで利用した値が見えてしまい、問題が発生することもあります。
3rd Partyライブラリなどで用意されている破棄メソッドを呼び出す
などの方法があるようですが、正確にはそれぞれのライブラリのドキュメントなどを参照ください。
ServletContext#destroyの中などで行うとよいでしょう。
リフレクションを用いて削除する
ServletContext#destroyの中などで、リフレクションを用いてThreadLocalを破棄する方法も挙げられます。
のような実装が参考になるのではないでしょうか。
Tomcatの場合は自動削除に対応しているようです*4。
アプリケーションのデプロイを行う際は、アプリケーションサーバの再起動も併せて実施する
身も蓋もないかもしれませんが、これを行うに越したことはありません。
ThreadLocalに限らず、それ以外のメモリリークを解消したり、起動時のみに発生する問題を検出するのにも有効です。
また、パッチを適用したり、構成を変更するときには、どうしても再起動が必須になるケースもあります。