MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能。某些应用程序采用多线程的方式来处理多个用户的请求。在一个用户的使用过程中,可能有多个不同的线程来进行处理。典型的例子是 Web 应用服务器。当用户访问某个页面时,应用服务器可能会创建一个新的线程来处理该请求,也可能从线程池中复用已有的线程。在一个用户的会话存续期间,可能有多个线程处理过该用户的请求。这使得比较难以区分不同用户所对应的日志。当需要追踪某个用户在系统中的相关日志记录时,就会变得很麻烦。
一种解决的办法是采用自定义的日志格式,把用户的信息采用某种方式编码在日志记录中。这种方式的问题在于要求在每个使用日志记录器的类中,都可以访问到用户相关的信息。这样才可能在记录日志时使用。这样的条件通常是比较难以满足的。MDC 的作用是解决这个问题。
MDC 可以看成是一个与当前线程绑定的哈希表,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。
MDC的用法时MDC.put(key,value),获取时直接MDC.get(key);
MDC是通过ThreadLocal对象实现的,在此顺便复习下ThreadLocal原理
MDC.put()方法的源码如下:
public static void put(String key, String val) throws IllegalArgumentException {
if (key == null) {
throw new IllegalArgumentException("key parameter cannot be null");
}
if (mdcAdapter == null) {
throw new IllegalStateException("MDCAdapter cannot be null. See also " + NULL_MDCA_URL);
}
mdcAdapter.put(key, val);
}
可以看到是调用了mdcAdapter.put()方法,mdcAdapter 的声明是static MDCAdapter mdcAdapter,其中MDCAdapter是一个接口类型,有多种实现,我们看一下logback的实现LogbackMDCAdapter,它的put方法源码如下
public void put(String key, String val) throws IllegalArgumentException {
if (key == null) {
throw new IllegalArgumentException("key cannot be null");
}
Map<String, String> oldMap = copyOnThreadLocal.get();
Integer lastOp = getAndSetLastOperation(WRITE_OPERATION);
if (wasLastOpReadOrNull(lastOp) || oldMap == null) {
Map<String, String> newMap = duplicateAndInsertNewMap(oldMap);
newMap.put(key, val);
} else {
oldMap.put(key, val);
}
}
可以看到就是根据规定的操作选择创建新的map对象或者使用旧的map对象,map对象时放入copyOnThreadLocal里,它的声明如下:
final ThreadLocal<Map<String, String>> copyOnThreadLocal = new ThreadLocal<Map<String, String>>();
可以看到MDC实现多线程下区分不同信息就是通过ThreadLocal。
下面顺便来复习下ThreadLocal的实现原理,
threadLocal的使用方式很简单,
ThreadLocal threadLocal = new ThreadLocal();
threadLocal.set(new T());
使用时直接 threadLocal.get()即可;
threadLocal.set方法的源码所示:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
实际上就是把ThreadLocal实例和它要保存的变量放入了ThreadLocalMap 中,ThreadLocalMap是通过getMap()来获得,传入的参数是当前的thread对象,其源码如下:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
可以看到实际上ThreadLocalMap就是线程对象的一个成员对象,也就是说,ThreadLocal保存线程安全变量的最终实现方式就是把变量存入了Thread对象中。
回到ThreadLocalMap,它是个什么对象呢?现在看看ThreadLocalMap的set方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
可以看到其实set方法是把数据放入了一个Entry[]数组,而这个Entry[]数组,是ThreadLocalMap的table成员,table 的声明如下:
private Entry[] table;
所以这样看来,ThreadLocalMap维护了一个Entry[]数组来保存ThreadLocal到ThreadLocal所保存对象的映射。
Entry的定义如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
可以看到实际上就是一个弱引用对象。
综上看来,ThreadLocal的实现原理就是在Thread里存了其本身的弱引用和需存储的变量。
ThreadLocal有时会造成内存泄漏,详见https://blog.youkuaiyun.com/zhailuxu/article/details/79067467