🧶

[Java] MDC.putはstatic呼び出しなのにスレッドごとの情報を持てるヒミツ - ThreadLocal

2021/12/30に公開

MDC.put()

Javaのロギングシステムlog4jやlogback、およびそれらのFacade(ファサードと読む。受付係のような役割のこと)であるslf4jではMDC(Mapped Diagnostic Context)を使用することができます。

MDC.put(key, value)

このように設定した内容は、パターンを指定することでログに値が出力されます。

https://qiita.com/namutaka/items/c35c437b7246c5e4d729

また、プログラム中で、

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を取得しています。そのあとに、mapnullの場合は新たにMapを作成し、copyOnThreadLocal.set(newMap);でThreadLocalにセットします。

スレッドごとのマップが取得できた後は、引数で与えられたkeyvalをセットしています。

get()の場合も同様にThreadLocalから現在のスレッドに対応するMapを取得し、値を取得しています。

この記事内では初出ですが、MDC.clear()およびThreadLocal.remove()も大切なメソッドです。ThreadLocal.remove()を行うことで、このスレッドに関連付けられたMapを削除しています。

https://github.com/qos-ch/slf4j/blob/master/slf4j-api/src/main/java/org/slf4j/MDC.java
https://github.com/qos-ch/logback/blob/master/logback-classic/src/main/java/ch/qos/logback/classic/util/LogbackMDCAdapter.java

ThreadLocalを使用した自作クラス

スレッドごとの情報を保持したい場合は、logbackの実装と同様に、ThreadLocalインスタンスを保持したクラスを作成します。

ThreadLocalを利用する際の注意点

最後に必ずremove()する

ThreadLocalを使用する場合、最後に必ずremove()しましょう。(MDCを利用している場合は最後に必ずclear()する。)
Javaアプリケーションサーバではスレッドはプールされ、再利用されているのが一般的です。remove()し忘れると、以前のリクエストの内容を参照してしまうことがあります。これを防ぐために、リクエストの最初にもremove()を入れておくことも有効です。

また、参照したオブジェクトが解放されないため、メモリリークの危険性もあります。

https://yamadamn.hatenablog.com/entry/20131214/1386996898

Discussion