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());
    }
}

我们都知道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"));
    }
}

从使用上来说,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());
    }
}

看了上面的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修改的值只影响了线程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);
}

接下来我们继续深入看看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<Map<String, String>> copyOnThreadLocal = new ThreadLocal<Map<String, String>>();
    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);
    }
}

从上面的代码可以看出来:数据存放在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);
}

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

public String get(String key) {
    //final ThreadLocal<Map<String, String>> copyOnThreadLocal = new ThreadLocal<Map<String, String>>();
    final Map<String, String> map = copyOnThreadLocal.get();
    if ((map != null) && (key != null)) {
        return map.get(key);
    } else {
        return null;
    }
}

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.
 *
 * <p>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.
     * <p>
     * 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);
    }
}

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

这个类提供了一种能力:子线程可以继承父线程中的值。当一个子线程被创建时,这个子线程就会继承并初始化所有的父线程中的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. ThreadLocal使用来存储线程本地变量的。
  2. MDC的底层使用的就是ThreadLocal<Map<String, String>>来存储的,只不过是做了一些安全校验,如MDC取值时ThreadLocalMap==null怎么办。
  3. InheritableThreadLocal支持子线程继承父线程中的thread-local值。

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

阿里巴巴开源工具:TransmittableThreadLocal

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值