slf4j的MDC对象和ThreadLocal简单分析

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.csdn.net/zhailuxu/article/details/79067467

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值