• MDC、ThreadLocal、InheritableThreadLocal的区别和联系


    前言

    最近在看研究分布式链路追踪日志,怎么组成一个链路呢,就是从接口请求开始进来,调用各种接口及redis、消息中间件、数据库、最终到接口返回。这中间所有的日志都要有一个唯一标识来把整个流程的日志串联起来,来标识一次请求。这个唯一标识就是REQUEST_ID或者交TRACE_ID

    看到很多日志都是依赖MDC来实现的REQUEST_ID的存储。然后开始研究!

    简单使用

    ThreadLocal

    /**
     * @author zhao.hualuo
     * Create at 2022/11/9
     */
    public class ThreadLocalTest {
    
        private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
    
        public static void main(String[] args) {
            THREAD_LOCAL.set("hello world");
            System.out.println("主线程1:" + THREAD_LOCAL.get());
            Thread thread = new Thread() {
                @Override
                public void run() {
                    System.out.println("新线程:" + THREAD_LOCAL.get());
                }
            };
            thread.start();
            System.out.println("主线程2:" + THREAD_LOCAL.get());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    我们都知道ThreadLocal就是用来存储线程本地变量的,所以在main线程创建的THREAD_LOCAL变量,在其他线程中是无法使用的。

    image.png

    MDC

    /**
     * @author zhao.hualuo
     * Create at 2022/11/9
     */
    public class MdcTest {
    
        public static void main(String[] args) {
            MDC.put("testKey", "hello world");
    
            System.out.println("主线程1:" + MDC.get("testKey"));
            Thread thread = new Thread() {
                @Override
                public void run() {
                    System.out.println("新线程:" + MDC.get("testKey"));
                }
            };
            thread.start();
            System.out.println("主线程2:" + MDC.get("testKey"));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    从使用上来说,MDC和ThreadLocal并没有什么区别,都是只能在本线程中使用。
    image.png

    InheritableThreadLocal

    /**
     * @author zhao.hualuo
     * Create at 2022/11/9
     */
    public class InheritableThreadLocalTest {
    
        private static final ThreadLocal<String> THREAD_LOCAL = new java.lang.InheritableThreadLocal<>();
    
        public static void main(String[] args) {
            THREAD_LOCAL.set("hello world");
            System.out.println("主线程1:" + THREAD_LOCAL.get());
            Thread thread = new Thread() {
                @Override
                public void run() {
                    System.out.println("新线程:" + THREAD_LOCAL.get());
                }
            };
            thread.start();
            System.out.println("主线程2:" + THREAD_LOCAL.get());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    看了上面的ThreadLocal和MDC,会不会有人想:有没有能跨线程存储的工具呢?毕竟在日志链路追踪中是会需要这种功能的。。。正常的业务功能中,一次请求可能会开多个线程的。但是这么多线程还是属于一次请求,在日志中是要能一起查询出来的。。。
    从下面的执行结果可以看出,InheritableThreadLocal是可以将main线程中的数据传递到子线程中的。
    image.png
    从上面的测试,我们可以看到InheritableThreadLocal是可以将main线程中的数据传递到子线程中的。
    那么新问题来了:子线程中数据的变更会不会传递到父线程?没有父子关系的线程会不会有影响?于是我们继续尝试:

    /**
     * @author zhao.hualuo
     * Create at 2022/11/9
     */
    public class InheritableThreadLocalTest {
    
        private static final ThreadLocal<String> THREAD_LOCAL = new java.lang.InheritableThreadLocal<>();
    
        public static void main(String[] args) throws InterruptedException {
            THREAD_LOCAL.set("hello world");
            System.out.println("主线程1:" + THREAD_LOCAL.get());
    
            Thread thread = new Thread() {
                @Override
                public void run() {
                    THREAD_LOCAL.set("game over");
                    System.out.println("新线程:" + THREAD_LOCAL.get());
                }
            };
    
            Thread thread2 = new Thread() {
                @Override
                public void run() {
                    System.out.println("新线程:" + THREAD_LOCAL.get());
                }
            };
    
            thread.start();
            //睡一会,给线程1充足的时间来修改变量值
            Thread.sleep(1000 * 5);
            System.out.println("主线程2:" + THREAD_LOCAL.get());
            thread2.start();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    经过测试,我们可以看出,线程1修改的值只影响了线程1自己。父类main线程没有受到影响。线程2也没有受到影响。
    image.png

    MDC

    从上面的测试可以看出来,MDC和ThreadLocal在表现上是没有什么区别的。那么我们接下来从源码的角度探究一下原理。

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    接下来我们继续深入看看mdcAdapter.put(key, val);方法,由于MDCAdapter这个接口有不同的实现类,这里以LogbackMDCAdapter类为例。

    public void put(String key, String val) throws IllegalArgumentException {
        if (key == null) {
            throw new IllegalArgumentException("key cannot be null");
        }
    
        //final ThreadLocal> copyOnThreadLocal = new ThreadLocal>();
        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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    从上面的代码可以看出来:数据存放在copyOnThreadLocal中,而copyOnThreadLocal就是ThreadLocal。从这里我们就可以看出来 为什么MDC和ThreadLocal用起来几乎是一样的效果了,因为底层是一个东西。

    get方法

    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    和上面一样,我们看一下mdcAdapter.get(key);方法。不出所料,底层还是取的copyOnThreadLocal。

    public String get(String key) {
        //final ThreadLocal> copyOnThreadLocal = new ThreadLocal>();
        final Map<String, String> map = copyOnThreadLocal.get();
        if ((map != null) && (key != null)) {
            return map.get(key);
        } else {
            return null;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    InheritableThreadLocal

    通过上面的示例,我们知道了InheritableThreadLocal支持threadLocal变量继承。那么是如何实现的呢?

    /**
     * This class extends ThreadLocal to provide inheritance of values
     * from parent thread to child thread: when a child thread is created, the
     * child receives initial values for all inheritable thread-local variables
     * for which the parent has values.  Normally the child's values will be
     * identical to the parent's; however, the child's value can be made an
     * arbitrary function of the parent's by overriding the childValue
     * method in this class.
     *
     * 

    Inheritable thread-local variables are used in preference to * ordinary thread-local variables when the per-thread-attribute being * maintained in the variable (e.g., User ID, Transaction ID) must be * automatically transmitted to any child threads that are created. * * @author Josh Bloch and Doug Lea * @see ThreadLocal * @since 1.2 */ public class InheritableThreadLocal<T> extends ThreadLocal<T> { /** * Computes the child's initial value for this inheritable thread-local * variable as a function of the parent's value at the time the child * thread is created. This method is called from within the parent * thread before the child is started. *

    * This method merely returns its input argument, and should be overridden * if a different behavior is desired. * * @param parentValue the parent thread's value * @return the child thread's initial value */ protected T childValue(T parentValue) { return parentValue; } /** * Get the map associated with a ThreadLocal. * * @param t the current thread */ ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; } /** * Create the map associated with a ThreadLocal. * * @param t the current thread * @param firstValue value for the initial entry of the table. */ void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); } }

    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    简单翻译一下源码的解释:

    这个类提供了一种能力:子线程可以继承父线程中的值。当一个子线程被创建时,这个子线程就会继承并初始化所有的父线程中的thread-local中所有的变量。通常来说,子线程中的thread-local中变量的值和父线程是完全相同的。然而,也可以通过覆写childValue方法,使子线程中的thread-local中的值都是通过父类中的值进行函数转化后获得的。

    简单理解就是:使用InheritableThreadLocal,默认子线程中的thread-local中的值都是和父线程中的值一样,除非覆写了childValue方法。
    下面分析一下源码,看看源码是如何实现的thread-local继承。我们看到上面的源码中创建和获取都是使用的Thread类中的inheritableThreadLocals属性,那我们就进入Thread类看一下这个属性:

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    
    //我们看到了inheritableThreadLocals变量的定义,那么是什么时候赋值的呢?找一下Thread类的初始化。即Thread类的构造函数
    public Thread() {
        //调用了init方法,往下找
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }
    
    private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
        //调用了init方法,往下找
        init(g, target, name, stackSize, null, true);
    }
    
    //这个类里面就有inheritableThreadLocals的赋值了
    private void init(ThreadGroup g, Runnable target, String name,
                          long stackSize, AccessControlContext acc,
                          boolean inheritThreadLocals) {
        if (name == null) {
            throw new NullPointerException("name cannot be null");
        }
    
        this.name = name;
    
        Thread parent = currentThread();
        SecurityManager security = System.getSecurityManager();
        if (g == null) {
            /* Determine if it's an applet or not */
    
            /* If there is a security manager, ask the security manager
               what to do. */
            if (security != null) {
                g = security.getThreadGroup();
            }
    
            /* If the security doesn't have a strong opinion of the matter
               use the parent thread group. */
            if (g == null) {
                g = parent.getThreadGroup();
            }
        }
    
        /* checkAccess regardless of whether or not threadgroup is
           explicitly passed in. */
        g.checkAccess();
    
        /*
         * Do we have the required permissions?
         */
        if (security != null) {
            if (isCCLOverridden(getClass())) {
                security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
            }
        }
    
        g.addUnstarted();
    
        this.group = g;
        this.daemon = parent.isDaemon();
        this.priority = parent.getPriority();
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.inheritedAccessControlContext =
                acc != null ? acc : AccessController.getContext();
        this.target = target;
        setPriority(priority);
        //略过上面的逻辑,直接看我们的赋值操作是怎么进行的
        
        //如果inheritThreadLocals为true证明是支持线程继承
        //(默认是true,只有通过下面这个构造函数创建的线程才不支持继承:Thread(Runnable target, AccessControlContext acc) )
    
        //如果父线程中的inheritableThreadLocals不为空,那么调用createInheritedMap方法,继续追踪
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        /* Stash the specified stack size in case the VM cares */
        this.stackSize = stackSize;
    
        /* Set thread ID */
        tid = nextThreadID();
    }
    
    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        //创建一个ThreadLocalMap,并且初始就包含父线程中的threadlocal值
        return new ThreadLocalMap(parentMap);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91

    总结

    1. ThreadLocal使用来存储线程本地变量的。
    2. MDC的底层使用的就是ThreadLocal>来存储的,只不过是做了一些安全校验,如MDC取值时ThreadLocalMap==null怎么办。
    3. InheritableThreadLocal支持子线程继承父线程中的thread-local值。

    留个问题哈:现在项目中很多使用多线程的地方都是使用的线程池,线程池中的线程并不具备父子关系,这样threadlocal怎么传递?

    阿里巴巴开源工具:TransmittableThreadLocal

  • 相关阅读:
    百度地图根据地址获取经纬度
    【Python 千题 —— 基础篇】学生转学了
    Mysql 三级等保安全加固
    Python基础内容训练4(常用的数据类型-----元组)
    (还在纠结Notability、Goodnotes和Marginnote吗?)iPad、安卓平板、Windows学习软件推荐
    计算属性全选反选
    如何让别人看不懂你的 JS 代码?把你当大佬!
    需求评审,测试人员应该发挥怎样的价值?两分钟让你不再懵逼
    springboot+jwt做登录鉴权(附完整代码)
    跨越速运如何构建实时统一的运单分析
  • 原文地址:https://blog.csdn.net/qq_33247435/article/details/127793733