资深的网站推广武汉seo关键词优化
文章目录
- 一、MDC是什么
- 1.1 MDC常用API
- 1.2 MDC数据结构
- 二、MDC的SPI机制
- 2.1 LogbackMDCAdapter绑定过程
- 三、MDC源码解析
- 3.1 MDC源码
- 3.2 LogbackMDCAdapter源码
- 3.2.1 Write -> Write 场景分析
- 3.2.2 Write -> Copy map -> Write 场景分析
- 四、MDC的局限性
- 4.1 父子线程数据无法传递
- 4.2 线程池使用MDC存在数据传递重复
- 4.3 如何破局?
- 五、总结
一、MDC是什么
MDC全称为Mapped Diagnostic Context,是著名日志框架SLF4J提供的一种方便多线程条件下记录日志的工具类,在分布式链路跟踪系统实现上,MDC绝对算是是一把核心利器。MDC的直观理解,可看做一个与当前线程绑定的Map,这个Map用来存储线程私有信息。怎么做到与线程绑定呢,用到的就是JDK的ThreadLocal,楼主之前也有专门问章分析过ThreadLocal,见【了不起的ThreadLocal】一、源码分析
1.1 MDC常用API
使用MDC非常简单,定义的常用API也就如下6个静态方法
- void put(String key, String val)
- String get(String key)
- void remove(String key)
- void clear()
- Map<String, String> getCopyOfContextMap()
- void setContextMap(Map<String, String> contextMap)
1.2 MDC数据结构
MDC数据结构如下图所示,简单概括下:
- MDC持有一个MDCAdapter,这个MDCAdapter是一个接口,实现交给具体日志组件,比如在Logback的实现类就是LogbackMDCAdapter;
- MDC可视作一个门面类,对外交互的所有API都会经过MDC,再委托给具体的MDCAdapter来执行。
二、MDC的SPI机制
在Java生态里面,日志组件非常丰富,比较普及的有Log4j、Logback、Log4j2等;前面讲到MDC持有的MDCAdapter仅仅是一个接口,运行时具体用到的是哪个MDCAdapter的实现,完全交给应用系统来决定。比如应用系统采用的是Logback,则MDC绑定的将是LogbackMDCAdapter。这个绑定过程是通过SLF4J定义的一套SPI扩展机制来实现的,下文会结合MDC部分源码来更好地理解下这个SPI机制。
2.1 LogbackMDCAdapter绑定过程
如上图所示,MDC完整类路径为org.slf4j.MDC
,注意看上图slf4j-api这个jar包,是没有org.slf4j.impl
这个包的。这里就得提到第一个规范:SLF4J的SPI机制,强制要求实现SLF4J日志标准的组件,必须将spi实现类的绑定逻辑统一放在org.slf4j.impl
这个包下面,并且类名和部分方法名是固定,不能瞎写。
不信?对照Logback这个jar包结构看下。
以Logback为例,来看看具体的绑定过程:
- 1.当客户端调用MDC的任一静态方法时,将触发MDC这个类的初始化,进而触发static静态代码块执行(这个机制是JVM的规范,static代码块只会被执行一次);
- 2.在static静态代码块中,主要有两条途径加载具体MDC绑定类: 第一种是通过调用
org.slf4j.impl
这个包的StaticMDCBinder.getSingleton().getMDCA()
,很遗憾logback里面没有这个方法,故抛出异常;第二种是调用org.slf4j.impl
包StaticMDCBinder.SINGLETON.getMDCA()
方法,刚好Logback就是这么定义的,找到了!返回的就是LogbackMDCAdapter,故最终MDC成功绑定的就是LogbackMDCAdapter;
到这来,SPI就说完了!没错,这个SPI机制就是这么简单粗暴得不敢相信!
注意:下面的代码在slf4j里面
注意: org.slf4j.impl.StaticMDCBinder这个对象引用,实际SLF4J 这个jar包里面是没有这个类的;但MDC.java文件又引入了,为何编译器不报错?原因在于用到了animal-sniffer-maven-plugin这个Maven插件,这个插件的细节楼主还没仔细看,感兴趣的同学可以自行研究下!
import java.io.Closeable;
import java.util.Map;import org.slf4j.helpers.NOPMDCAdapter;
import org.slf4j.helpers.BasicMDCAdapter;
import org.slf4j.helpers.Util;// 注意这个包的引用(实际SLF4J里面没有这个类)
import org.slf4j.impl.StaticMDCBinder;import org.slf4j.spi.MDCAdapter;public class MDC {static final String NULL_MDCA_URL = "http://www.slf4j.org/codes.html#null_MDCA";static final String NO_STATIC_MDC_BINDER_URL = "http://www.slf4j.org/codes.html#no_static_mdc_binder";static MDCAdapter mdcAdapter;/*** As of SLF4J version 1.7.14, StaticMDCBinder classes shipping in various bindings* come with a getSingleton() method. Previously only a public field called SINGLETON * was available.* * @return MDCAdapter* @throws NoClassDefFoundError in case no binding is available* @since 1.7.14*/private static MDCAdapter bwCompatibleGetMDCAdapterFromBinder() throws NoClassDefFoundError {try {// logback没有这个方法,故会进入到catch代码块里面return StaticMDCBinder.getSingleton().getMDCA();} catch (NoSuchMethodError nsme) {// logback里面能找到// binding is probably a version of SLF4J older than 1.7.14return StaticMDCBinder.SINGLETON.getMDCA();}}// MDC类初始化时会执行static {try {// 绑定具体的MDCAdapter实现mdcAdapter = bwCompatibleGetMDCAdapterFromBinder();} catch (NoClassDefFoundError ncde) {mdcAdapter = new NOPMDCAdapter();String msg = ncde.getMessage();if (msg != null && msg.contains("StaticMDCBinder")) {Util.report("Failed to load class \"org.slf4j.impl.StaticMDCBinder\".");Util.report("Defaulting to no-operation MDCAdapter implementation.");Util.report("See " + NO_STATIC_MDC_BINDER_URL + " for further details.");} else {throw ncde;}} catch (Exception e) {// we should never get hereUtil.report("MDC binding unsuccessful.", e);}}
}
注意:这里的代码在Logback里面,并非slf4j
public class StaticMDCBinder {/*** The unique instance of this class.*/public static final StaticMDCBinder SINGLETON = new StaticMDCBinder();private StaticMDCBinder() {}/*** Currently this method always returns an instance of * {@link StaticMDCBinder}.*/public MDCAdapter getMDCA() {return new LogbackMDCAdapter();}public String getMDCAdapterClassStr() {return LogbackMDCAdapter.class.getName();}
}
三、MDC源码解析
3.1 MDC源码
知道MDC干活全靠委派给MDCAdapter,那MDC的源码其实就不用细看了,以下瞟一眼,大概知道有哪几个API,并且意识到get、put、remove方法都要求key不能null,就可以略过了。
public class MDC {static final String NULL_MDCA_URL = "http://www.slf4j.org/codes.html#null_MDCA";static final String NO_STATIC_MDC_BINDER_URL = "http://www.slf4j.org/codes.html#no_static_mdc_binder";static MDCAdapter mdcAdapter;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);}public static String get(String key) 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);}return mdcAdapter.get(key);}public static void remove(String key) 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.remove(key);}public static void clear() {if (mdcAdapter == null) {throw new IllegalStateException("MDCAdapter cannot be null. See also " + NULL_MDCA_URL);}mdcAdapter.clear();}/*** Return a copy of the current thread's context map, with keys and values of* type String. Returned value may be null.* * @return A copy of the current thread's context map. May be null.* @since 1.5.1*/public static Map<String, String> getCopyOfContextMap() {if (mdcAdapter == null) {throw new IllegalStateException("MDCAdapter cannot be null. See also " + NULL_MDCA_URL);}return mdcAdapter.getCopyOfContextMap();}/*** Set the current thread's context map by first clearing any existing map and* then copying the map passed as parameter. The context map passed as* parameter must only contain keys and values of type String.* * @param contextMap* must contain only keys and values of type String* @since 1.5.1*/public static void setContextMap(Map<String, String> contextMap) {if (mdcAdapter == null) {throw new IllegalStateException("MDCAdapter cannot be null. See also " + NULL_MDCA_URL);}mdcAdapter.setContextMap(contextMap);}}
3.2 LogbackMDCAdapter源码
LogbackMDCAdapter是真正的主角,MDC的数据读/写都交给了它。太阳底下无新鲜事,LogbackMDCAdapter最核心的数据存储结构就是2个ThreadLocal,围绕这2个ThreadLocal完成MDC的全部接口功能。故看懂LogbackMDCAdapter的关键在于看懂这2个ThreadLocal的设计用意!
public class LogbackMDCAdapter implements MDCAdapter {// MDC真正用来存数据的容器final ThreadLocal<Map<String, String>> copyOnThreadLocal = new ThreadLocal<Map<String, String>>();private static final int WRITE_OPERATION = 1;private static final int MAP_COPY_OPERATION = 2;// 记录上一次操作类型:写为1(put、remove、cleare都是写),copy map为2// keeps track of the last operation performedfinal ThreadLocal<Integer> lastOperation = new ThreadLocal<Integer>();private Integer getAndSetLastOperation(int op) {// 上一次操作类型Integer lastOp = lastOperation.get();// 记录当前操作lastOperation.set(op);// 返回上一次操作类型return lastOp;}private boolean wasLastOpReadOrNull(Integer lastOp) {return lastOp == null || lastOp.intValue() == MAP_COPY_OPERATION;}}
了解源码最好的办法,就是把代码run起来。楼主写了几个测试类,下面就按照测试类的执行思路展开源码分析。
3.2.1 Write -> Write 场景分析
- 测试Case1
public class MdcTest {@Testpublic void testMdc() {// 打印出当前绑定的MDCAdapterSystem.out.println(MDC.getMDCAdapter().getClass().getName());// 第一次操作putMDC.put("a", "123");dumpMdc();// 第二次操作putMDC.put("b", "456");dumpMdc();// 第三次操作putMDC.remove("b");dumpMdc();}public static void dumpMdc() {System.out.println(MDC.getCopyOfContextMap());}}
- 输出
ch.qos.logback.classic.util.LogbackMDCAdapter
{a=123}
{a=123, b=456}
{a=123}
为便于直观理解,配几幅图来动态分析代码执行流程(靠想象力自行在脑海模拟JVM运行上述代码)
-
LogbackMDCAdapter 类初始状态
简单描述下:LogbackMDCAdapter这个类初始化之后,copyOnThreadLocal、lastOperation两个final变量(ps: final变量实际存放在方法区,图上画在栈内存纯属偷懒),分别指向`ThreadLocal<Map<String, String>> copyOnThreadLocal 和 ThreadLocal lastOperation 这两个ThreadLocal;此时,当前线程(main线程)的ThreadLocalMap(即threadLocals属性)还没存放值。
-
a.第一次操作执行
put("a", "123")
public class LogbackMDCAdapter implements MDCAdapter {private Integer getAndSetLastOperation(int op) {// 上一次操作类型Integer lastOp = lastOperation.get();// 记录当前操作lastOperation.set(op);// 返回上一次操作类型return lastOp;}private boolean wasLastOpReadOrNull(Integer lastOp) {return lastOp == null || lastOp.intValue() == MAP_COPY_OPERATION;}// 第一次put操作时,oldMap为nullprivate Map<String, String> duplicateAndInsertNewMap(Map<String, String> oldMap) {// new了一个新map对象Map<String, String> newMap = Collections.synchronizedMap(new HashMap<String, String>());if (oldMap != null) {// we don't want the parent thread modifying oldMap while we are// iterating over itsynchronized (oldMap) {newMap.putAll(oldMap);}}// 对ThreadLocal的某个Entry赋值,假设就是Entry[0], 其key就是copyOnThreadLocal,其value为空mapcopyOnThreadLocal.set(newMap);return newMap;}public void put(String key, String val) throws IllegalArgumentException {if (key == null) {throw new IllegalArgumentException("key cannot be null");}// 第一次put操作时,因为main线程的ThreadLocalMap还是空的,故从copyOnThreadLocal定位不到任何Entry,故返回的oldMap为nullMap<String, String> oldMap = copyOnThreadLocal.get();// 记录当前操作为写(new一个Entry[3],并将value设置为1),返回的lastOp为nullInteger lastOp = getAndSetLastOperation(WRITE_OPERATION);if (wasLastOpReadOrNull(lastOp) || oldMap == null) {// 进到这个分支,并且oldMap为null; Map<String, String> newMap = duplicateAndInsertNewMap(oldMap);// 调用duplicateAndInsertNewMap之后的结果是创建出了Entry[0], 并且其value为一个空map// 将a=123塞到newMap, 由于newMap和Entry[0].value指向的是同一个map对象,故最终效果就是a=123写到了main线程的ThreadLocal里面了(理解这个语句非常重要!!)newMap.put(key, val);} else {oldMap.put(key, val);}}}
这里留一个问题,为什么lastOp为copy map时要new一个map出来?
- b.第二次操作执行
put("b", "456")
public class LogbackMDCAdapter implements MDCAdapter {public void put(String key, String val) throws IllegalArgumentException {if (key == null) {throw new IllegalArgumentException("key cannot be null");}// 第二次put操作时,从copyOnThreadLocal定位到Entry[0]的value为{a=123}Map<String, String> oldMap = copyOnThreadLocal.get();// 从lastOperation定位到Entry[3]的value为1,故返回的lastOp为1;并记录当前操作为写(将Entry[3]的value从1设置为1)Integer lastOp = getAndSetLastOperation(WRITE_OPERATION);if (wasLastOpReadOrNull(lastOp) || oldMap == null) {Map<String, String> newMap = duplicateAndInsertNewMap(oldMap);newMap.put(key, val);} else {// lastOp为1且oldMap又不为null,故进到这个分支// 直接把当前写操作内容放入oldMap,最终Entry[0].value为{a=123, b=456}oldMap.put(key, val);}}}
- c.第三次操作
remove("b")
public class LogbackMDCAdapter implements MDCAdapter {public void remove(String key) {if (key == null) {return;}// 从copyOnThreadLocal定位到Entry[0]的value为{a=123, b=456}Map<String, String> oldMap = copyOnThreadLocal.get();if (oldMap == null)return;// 从lastOperation定位到Entry[3]的value为1,故返回的lastOp=1Integer lastOp = getAndSetLastOperation(WRITE_OPERATION);if (wasLastOpReadOrNull(lastOp)) {Map<String, String> newMap = duplicateAndInsertNewMap(oldMap);newMap.remove(key);} else {// lastOp=1,进到这个分支;直接从oldMap做删除操作,删除b之后oldMap为{a=123}oldMap.remove(key);}}}
3.2.2 Write -> Copy map -> Write 场景分析
在这一小节,回答前面提出的「为什么lastOp为copy map的时候要new一个map出来」,还是结合一个测试Case来分析。
- 测试Case2
public class MdcTest {@Test
public void testLogbackMDCAdapter() {LogbackMDCAdapter mdcAdapter = new LogbackMDCAdapter();// 第一次操作putmdcAdapter.put("a", "123");// 注意: 方法名虽然看着像在做copy map,实际没有记录成copy map操作System.out.println(mdcAdapter.getCopyOfContextMap());// 第二次操作 copy map(这个方法里面里面才真正记录了 copy map操作)Map<String, String> snapshot = mdcAdapter.getPropertyMap();System.out.println("snapshot: " + snapshot);// 第三次操作putmdcAdapter.put("b", "456");System.out.println(mdcAdapter.getCopyOfContextMap());System.out.println("snapshot: " + snapshot);
}
}
- 输出
{a=123}
snapshot: {a=123}
{a=123, b=456}
snapshot: {a=123}
-
a.第一次操作执行
put("a", "123")
第一次put操作后,同前文分析完全一样,照搬前面的图内存中对象如下:
-
b.第二次操作
getPropertyMap()
注意: LogbackMDCAdapter的copy map操作实际发生在getPropertyMap()
方法,而不是getCopyOfContextMap()
方法。
public Map<String, String> getPropertyMap() {// 通过lastOperation定位到Entry[3], 将Entry[3].value从1更新为2lastOperation.set(MAP_COPY_OPERATION);// 返回Entry[0].value,被变量snapshot接收return copyOnThreadLocal.get();
}
- c.第三次操作执行
put("b", "456")
public class LogbackMDCAdapter implements MDCAdapter {public void put(String key, String val) throws IllegalArgumentException {if (key == null) {throw new IllegalArgumentException("key cannot be null");}// 当前put操作时,从copyOnThreadLocal定位到Entry[0]的value为{a=123}Map<String, String> oldMap = copyOnThreadLocal.get();// 从lastOperation定位到Entry[3]的value为2,故返回的lastOp为2;并记录当前操作为写(将Entry[3]的value从2设置为1)Integer lastOp = getAndSetLastOperation(WRITE_OPERATION);if (wasLastOpReadOrNull(lastOp) || oldMap == null) {// lastOp=2, 故进到这个分支;Map<String, String> newMap = duplicateAndInsertNewMap(oldMap);// 调用duplicateAndInsertNewMap(oldMap)得到的newMap为{a=123};// 把当前kv写入newMap, 故最终newMap为{a=123, b=456}newMap.put(key, val);} else {oldMap.put(key, val);}}// 此处传入的oldMap为{a=123}private Map<String, String> duplicateAndInsertNewMap(Map<String, String> oldMap) {// new了一个新map对象Map<String, String> newMap = Collections.synchronizedMap(new HashMap<String, String>());if (oldMap != null) {// we don't want the parent thread modifying oldMap while we are// iterating over itsynchronized (oldMap) {// 将oldMap的内容搬到newMapnewMap.putAll(oldMap);}}// 对ThreadLocal的Entry[0]赋值(Entry[0]的value断开对原来oldMap的引用,指向newMap),最终Entry[0].value为newMap={a=123}copyOnThreadLocal.set(newMap);// 返回newMapreturn newMap;}
}
到这里应该就能理解为什么需要记录操作lastOperation以及进行oldMap、newMap的复制操作了。根本作用,就是为了保证在「write -> copy map -> write」这种场景下,每次copy map得到的都是一个当前快照,并且这个快照是不受后面的写操作影响。对照Case2来说,第一次put("a", "123")
操作后, 第二次操作进行copy map得到的快照snapshot为{a=123},第三次操作put("b", "456")
之后,此时mdc里面的内容已经是{a=123, b=456},但是打印snapshot,仍然还是{a=123}没有任何变更。因此lastOperation配合oldMap、newMap的复制操作,做到的就是快照读的效果。假如没有这种设计,在同一个线程里面第一次打印snapshot和第二次打印snapshot,会输出不同的值,就有点薛定谔了!MDC设计最难理解的点就在这里,看懂这个快照读机制才算真正理解了MDC。
四、MDC的局限性
4.1 父子线程数据无法传递
前面已经分析清楚了MDC的源码,归根到底MDC的数据读写都是基于ThreadLocal。如果你熟悉ThreadLocal,肯定知道它还有个子类InheritableThreadLocal
,用来做父子线程的数据传递。由于LogbackMDCAdapter采用的是ThreadLocal而非InheritableThreadLocal,因此存在第一个缺陷就是: 没法做到父子线程的数据传递。写个Case证明下!
- Case3: 证明MDC父子线程数据无法传递
public class MdcTest {@Testpublic void testMdc3() throws InterruptedException {// 打印出当前绑定的MDCAdapterSystem.out.println(MDC.getMDCAdapter().getClass().getName());MDC.put("foo", "123");CountDownLatch latch = new CountDownLatch(1);new Thread(() -> {System.out.println(Thread.currentThread() + ": " + MDC.get("foo"));latch.countDown();}, "child").start();latch.await();System.out.println(Thread.currentThread() + ": " + MDC.get("foo"));}}
- 输出:可以看到在child线程里面,从MDC取到的值为null;也就是说父线程main设置到MDC的值,子线程child根本就取不到
ch.qos.logback.classic.util.LogbackMDCAdapter
Thread[child,5,main]: null
Thread[main,5,main]: 123
解决父子线程数据传递手段很简单,将ThreadLocal换成InheritableThreadLocal就可以,InheritableThreadLocal怎么做到的,源码还得看Thread类的init方法,思路就是每当new一个子线程时,就把父线程的inheritableThreadLocals这个ThreadLocaMap复制下,赋值给当前子线程的threadLocals。
public
class Thread implements Runnable {/* ThreadLocal values pertaining to this thread. This map is maintained* by the ThreadLocal class. */ThreadLocal.ThreadLocalMap threadLocals = null;/** InheritableThreadLocal values pertaining to this thread. This map is* maintained by the InheritableThreadLocal class.*/ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;// 新建一个线程public Thread() {init(null, null, "Thread-" + nextThreadNum(), 0);}public Thread(String name) {init(null, null, name, 0);}private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc,boolean inheritThreadLocals) {Thread parent = currentThread();// 略去无关代码...// inheritThreadLocals 传入的默认值就是trueif (inheritThreadLocals && parent.inheritableThreadLocals != null)// 如果父线程的inheritableThreadLocals不为空,则通过ThreadLocal.createInheritedMap将inheritableThreadLocals复制一份赋值给当前线程(子线程)的threadLocals属性this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);// 略去无关代码...}}
4.2 线程池使用MDC存在数据传递重复
线程池使用MDC存在数据传递重复,这个问题就比较隐晦了。对照下4.1小节讲到的InheritableThreadLocal的原理,无非就是在线程新创建的时候,对父线程的ThreadLocalMap进行复制操作然后赋值给子线程,也就是说当前仅当线程创建的那个时机,才开始做复制操作。如果线程是通过线程池创建出来,线程循环利用,就只会有一次创建机会,那么就只会在第一次new的时候复制父线程ThreadLocalMap,计时后面父线程后面又更新了999次ThreadLocalMap,子线程都不会再去复制,这样的后果就是子线程从MDC取到的值一直都是第一次复制的值!严格来说,这个锅并非是MDC的,而是ThreadLocal的缺陷!下面还是写个Case证明下
- Case4: 证明InheritableThreadLocal在线程池场景下存在数据传递重复
public class MdcTest {@Testpublic void testInheritableThreadLocal() throws InterruptedException {final Executor executor = Executors.newFixedThreadPool(1);final ThreadLocal<Integer> threadLocal = new InheritableThreadLocal<>();AtomicInteger times = new AtomicInteger(0);for (int i = 0; i < 3; i++) {times.getAndIncrement();// 这里是在父线程上设置的值threadLocal.set(i);String timesStr = "第" + times.get() + "次循环";System.out.println(timesStr + Thread.currentThread() + ": " + threadLocal.get());try {executor.execute(() -> System.out.println(timesStr + Thread.currentThread() + ":" + " " + threadLocal.get()));} finally {threadLocal.remove();}}Thread.sleep(100);}}
- 输出
第1次循环,父线程main和子线程pool-1-thread-1从InheritableThreadLocal取到的值都是0,这是对的,证明了InheritableThreadLocal确实能从父线程复制到值;但是从第2次循环开始,父线程的值以及从0变成1、2,而子线程的值始终为0,这就有问题了,证明子线程后续完全不跟随父线程做值的变更!
第1次循环Thread[main,5,main]: 0
第1次循环Thread[pool-1-thread-1,5,main]: 0
第2次循环Thread[main,5,main]: 1
第3次循环Thread[main,5,main]: 2
第2次循环Thread[pool-1-thread-1,5,main]: 0
第3次循环Thread[pool-1-thread-1,5,main]: 0
4.3 如何破局?
针对上面2个缺陷,MDC本身并未给出解决方案。幸运的是阿里巴巴开源的transmittable-thread-local解决了以上个问题,其解决思路:1、针对父子线程数据无法传递问题,TransmittableThreadLocal继承并加强InheritableThreadLocal类;2、针对线程池InheritableThreadLocal数据数据传递存在重复问题,TransmittableThreadLocal提供了TtlRunnable和TtlCallable来修饰提交到线程池的任务,保证每次任务执行前强制从父线程copy下ThreadLocalMap的最新的值。
值得一提的是,除了TransmittableThreadLocal,还有一大神器TtlMDCAdapter,整合TransmittableThreadLocal的能力并直接实现MDCAdapter接口,用起MDC完全感知不到底层原来是TtlMDCAdapter,非常顺滑,再写个Demo展示下其能力!
- TtlMDCAdapter用法
public class MdcTest {@Testpublic void testTTLMDCAdapter() throws InterruptedException {// MDC绑定TtlMDCAdapterTtlMDCAdapter.getInstance();// 修饰线程池final Executor executor = TtlExecutors.getTtlExecutor(Executors.newFixedThreadPool(1));AtomicInteger times = new AtomicInteger(0);for (int i = 0; i < 3; i++) {times.getAndIncrement();String key = String.valueOf(i);// 这里是在父线上设置的值MDC.put(key, String.valueOf(i));String timesStr = "第" + times.get() + "次循环";System.out.println(timesStr + Thread.currentThread() + ": " + MDC.get(key));try {executor.execute(() -> System.out.println(timesStr + Thread.currentThread() + ":" + " " + MDC.get(key)));} finally {MDC.remove(key);}}Thread.sleep(100);}}
- 输出
父线程main和子线程pool-1-thread-1,三次循环操作,父子线程从MDC读取的值完全一一致!
第1次循环Thread[main,5,main]: 0
第2次循环Thread[main,5,main]: 1
第1次循环Thread[pool-1-thread-1,5,main]: 0
第3次循环Thread[main,5,main]: 2
第2次循环Thread[pool-1-thread-1,5,main]: 1
第3次循环Thread[pool-1-thread-1,5,main]: 2
五、总结
MDC最大的用途在于分布式链路跟踪上,真正会用的人就会知道MDC用起来有多么爽!MDC的源码分析到此终于告一段落,TransmittableThreadLocal和TtlMDCAdapter的源码本文就不再做分析了,好奇的同学可自行研究。最后概括本文主要内容:
- MDC的SPI机制: 以Logback为例,讲述了MDC的SPI机制实现原理。有了这个认识,读者可再去理解SLF4J对具体日志组件的绑定机制,就能做到触类旁通;
- MDC的源码分析: 以源码注释 + 测试Case + 示意图,细致地分析了MDC源码设计思路;
- MDC的2大局限性: 结合测试Case分析、证明MDC存在的问题,提出解决方案,进而引出TransmittableThreadLocal、TtlMDCAdapter两大神器。
全文终~