[Java] MDC.putはstatic呼び出しなのにスレッドごとの情報を持てるヒミツ - ThreadLocal
MDC.put()
Javaのロギングシステムlog4jやlogback、およびそれらのFacade(ファサードと読む。受付係のような役割のこと)であるslf4jではMDC(Mapped Diagnostic Context)を使用することができます。
MDC.put(key, value)
このように設定した内容は、パターンを指定することでログに値が出力されます。
また、プログラム中で、
MDC.get(key)
のようにMDCに設定した値を取り出すことができます。
さてここで疑問が。
MDCはクラスなので、MDC.put()
はstaticメソッドです。staticなメソッドなのでstaticなフィールドを書き換えているはずです。staticなフィールドはクラス固有の値です。クラス固有の値はすべてのスレッドから参照可能なはずです。ほかのスレッドと値が混ざってしまわないでしょうか?
ThreadLocal
実は、JavaにはThreadLocal
という便利なクラスが用意されています。ThreadLocal
では、そのスレッドに対応するデータを取得することができます。
logbackの実装はおおよそこのようになっています。(MDC.put()
はMDCAdapter.put()
を呼んでいます)
public class LogbackMDCAdapter implements MDCAdapter {
// MDC.put()された内容を保存するマップを持っているThreadLocal
final ThreadLocal<Map<String, String>> copyOnThreadLocal = new ThreadLocal<Map<String, String>>();
public void put(String key, String val) throws IllegalArgumentException {
// ThreadLocalから現在のスレッドに対応するMapを取得
Map<String, String> oldMap = copyOnThreadLocal.get();
if (oldMap == null) {
// まだMapがない場合(初めてputする場合など)
// 新しいMapを作成
Map<String, String> newMap = Collections.synchronizedMap(new HashMap<String, String>());
// ThreadLocalにセット
copyOnThreadLocal.set(newMap);
// 新しい値を設定
newMap.put(key, val);
} else {
// 新しい値を設定
oldMap.put(key, val);
}
}
public String get(String key) {
// ThreadLocalから現在のスレッドに対応するMapを取得
final Map<String, String> map = copyOnThreadLocal.get();
if ((map != null) && (key != null)) {
// 取得したMapから対応するkeyを取得
return map.get(key);
} else {
return null;
}
}
public void clear() {
// ThreadLocalからMapを削除
copyOnThreadLocal.remove();
}
}
クラスのフィールドにThreadLocal
を持っています。ここに、putされた内容をスレッドごとに保持します。
put()
の中では、copyOnThreadLocal.get()
でThreadLocalから現在のスレッドに対応するMapを取得しています。ここではスレッドのIDが出てきませんが、get()
の内部でスレッドのIDを取得しています。そのあとに、map
がnull
の場合は新たにMapを作成し、copyOnThreadLocal.set(newMap);
でThreadLocalにセットします。
スレッドごとのマップが取得できた後は、引数で与えられたkey
とval
をセットしています。
get()
の場合も同様にThreadLocalから現在のスレッドに対応するMapを取得し、値を取得しています。
この記事内では初出ですが、MDC.clear()
およびThreadLocal.remove()
も大切なメソッドです。ThreadLocal.remove()
を行うことで、このスレッドに関連付けられたMapを削除しています。
ThreadLocalを使用した自作クラス
スレッドごとの情報を保持したい場合は、logbackの実装と同様に、ThreadLocalインスタンスを保持したクラスを作成します。
ThreadLocalを利用する際の注意点
最後に必ずremove()する
ThreadLocalを使用する場合、最後に必ずremove()
しましょう。(MDCを利用している場合は最後に必ずclear()
する。)
Javaアプリケーションサーバではスレッドはプールされ、再利用されているのが一般的です。remove()
し忘れると、以前のリクエストの内容を参照してしまうことがあります。これを防ぐために、リクエストの最初にもremove()
を入れておくことも有効です。
また、参照したオブジェクトが解放されないため、メモリリークの危険性もあります。
Discussion