看图彻底掌握MDC设计理念

一、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.implStaticMDCBinder.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.14
            return 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 here
            Util.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 performed
    final 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 {
    
    @Test
    public void testMdc() {
        // 打印出当前绑定的MDCAdapter
        System.out.println(MDC.getMDCAdapter().getClass().getName());

        // 第一次操作put
        MDC.put("a", "123");
        dumpMdc();

        // 第二次操作put
        MDC.put("b", "456");
        dumpMdc();

        // 第三次操作put
        MDC.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为null
    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 it
            synchronized (oldMap) {
                newMap.putAll(oldMap);
            }
        }
        // 对ThreadLocal的某个Entry赋值,假设就是Entry[0], 其key就是copyOnThreadLocal,其value为空map
        copyOnThreadLocal.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为null
        Map<String, String> oldMap = copyOnThreadLocal.get();

        // 记录当前操作为写(new一个Entry[3],并将value设置为1),返回的lastOp为null
        Integer 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=1
        Integer 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();
    // 第一次操作put
    mdcAdapter.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);
    
    // 第三次操作put
    mdcAdapter.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更新为2
    lastOperation.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 it
            synchronized (oldMap) {
                // 将oldMap的内容搬到newMap
                newMap.putAll(oldMap);
            }
        }
        // 对ThreadLocal的Entry[0]赋值(Entry[0]的value断开对原来oldMap的引用,指向newMap),最终Entry[0].value为newMap={a=123}
        copyOnThreadLocal.set(newMap);
        // 返回newMap
        return 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 {

    @Test
    public void testMdc3() throws InterruptedException {
        // 打印出当前绑定的MDCAdapter
        System.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 传入的默认值就是true
        if (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 {

    @Test
    public 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 {

    @Test
    public void testTTLMDCAdapter() throws InterruptedException {
        // MDC绑定TtlMDCAdapter
        TtlMDCAdapter.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两大神器。

全文终~

  • 17
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
对于R2MDC FFT的架构设计,我可以给出一些常见的实践和建议。R2MDC FFT(Radix-2 Multi-Path Delay Commutator Fast Fourier Transform)是一种基于Radix-2 FFT算法的优化实现,用于高效地计算快速傅里叶变换(FFT)。 以下是R2MDC FFT的常见架构设计要点: 1. Radix-2算法:R2MDC FFT使用Radix-2算法,该算法将FFT长度N分解为2的幂次数。这种分解方式使得FFT计算可以递归地进行,提高了运行效率。 2. Multi-Path Delay Commutator结构:R2MDC FFT采用Multi-Path Delay Commutator结构,用于实现数据的并行处理。该结构将输入序列分为多个并行路径,每个路径按不同的延迟进行处理,最后将结果合并得到最终的FFT输出。 3. 状态机控制:R2MDC FFT的实现通常使用状态机来控制数据流和计算过程。状态机根据输入数据和计算阶段的不同,决定数据在不同路径间的选择和延迟。 4. 数据重排:为了适应Radix-2算法的要求,R2MDC FFT通常需要进行数据重排操作。数据重排可以通过不同的方法实现,例如比特反转(Bit-reversal)或者索引重新映射(Index remapping)等。 5. 数据流控制:由于R2MDC FFT是并行计算的,因此需要对数据流进行控制,以保证各个路径之间的数据同步和正确性。常见的数据流控制方法包括计数器、缓冲区和流水线等。 需要注意的是,R2MDC FFT的具体架构设计可能因应用场景和要求的不同而有所差异。在实际应用中,可以根据需求和资源限制进行优化和调整。以上是一般的架构设计要点,供参考。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值