工作中对InheritableThreadLocal使用的思考

作者:京东科技 王奕龙

代码评审时,发现在线程池中使用InheritableThreadLocal上下文会使其中的线程变量失效,无法获取到预期的变量值,所以对问题进行了复盘和总结。

1. 先说结论

InheritableThreadLocal 只有在父线程创建 子线程时,在子线程中才能获取到父线程中的线程变量;当配合线程池使用时: "第一次在线程池中开启线程,能在子线程中获取到父线程的线程变量,而当该子线程开启之后,发生线程复用,该子线程仍然保留的是之前开启它的父线程的线程变量,而无法获取当前父线程中新的线程变量" ,所以会发生获取线程变量错误的情况。

2. 实验例子

  • 创建一个线程数固定为1的线程池,先在main线程中存入变量1,并使用线程池开启新的线程打印输出线程变量,之后更改main线程的线程变量为变量2,再使用线程池中线程(发生线程复用)打印输出线程变量,对比两次输出的值是否不同
csharp 复制代码
/**
 * 测试线程池下InheritableThreadLocal线程变量失效的场景
 */
public class TestInheritableThreadLocal {

    private static final InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

    // 固定大小的线程池,保证线程复用
    private static final ExecutorService executorService = Executors.newFixedThreadPool(1);

    public static void main(String[] args) {
        threadLocal.set("main线程 变量1");
        // 正常取到 main线程 变量1
        executorService.execute(() -> System.out.println(threadLocal.get()));

        threadLocal.set("main线程 变量2");
        // 线程复用再取还是 main线程 变量1
        executorService.execute(() -> System.out.println(threadLocal.get()));
    }
}

输出结果:

main线程 变量1

main线程 变量1

发现两次输出结果值相同,证明发生线程复用时,子线程获取父线程变量失效

3. 详解

3.1 JavaDoc

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.
InheritableThreadLocal 继承了 ThreadLocal, 以能够让子线程能够从父线程中继承线程变量: 当一个子线程被创建时,它会接收到父线程中所有可继承的变量。通常情况下,子线程和父线程中的线程变量是完全相同的,但是可以通过重写 childValue 方法来使父子线程中的值不同。

当线程中维护的变量如UserId, TransactionId 等必须自动传递到新创建的任何子线程 时,使用InheritableThreadLocal要优于ThreadLocal

3.2 源码

scala 复制代码
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    /**
     * 当子线程被创建时,通过该方法来初始化子线程中线程变量的值,
     * 这个方法在父线程中被调用,并且在子线程开启之前。
     * 
     * 通过重写这个方法可以改变从父线程中继承过来的值。
     *
     * @param parentValue the parent thread's value
     * @return the child thread's initial value
     */
    protected T childValue(T parentValue) {
        return parentValue;
    }

    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

其中childValue方法来获取父线程中的线程变量的值,也可通过重写这个方法来将获取到的线程变量的值进行修改。

getMap方法和createMap方法中,可以发现inheritableThreadLocals变量,它是 ThreadLocalMap,在Thread类

3.2.1 childValue方法

  1. 开启新线程时,会调用Thread的构造方法
csharp 复制代码
    public Thread(ThreadGroup group, String name) {
        init(group, null, name, 0);
    }
  1. 沿着构造方法向下,找到init方法的最终实现,其中有如下逻辑:为当前线程创建线程变量以继承父线程中的线程变量
typescript 复制代码
/**
 * @param inheritThreadLocals 为ture,代表是为 包含可继承的线程变量 的线程进行初始化
 */
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    ...
  
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        // 注意这里创建子线程的线程变量
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    
    ...
    
}
  1. ThreadLocal.createInheritedMap(parent.inheritableThreadLocals)创建子线程 InheritedMap 的具体实现

createInheritedMap 方法,最终会调用到 ThreadLocalMap私有构造方法 ,传入的参数parentMap即为父线程中保存的线程变量

ini 复制代码
    private ThreadLocalMap(ThreadLocalMap parentMap) {
        Entry[] parentTable = parentMap.table;
        int len = parentTable.length;
        setThreshold(len);
        table = new Entry[len];

        for (int j = 0; j < len; j++) {
            Entry e = parentTable[j];
            if (e != null) {
                @SuppressWarnings("unchecked")
                ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                if (key != null) {
                    // 注意!!! 这里调用了childValue方法
                    Object value = key.childValue(e.value);
                    Entry c = new Entry(key, value);
                    int h = key.threadLocalHashCode & (len - 1);
                    while (table[h] != null)
                        h = nextIndex(h, len);
                    table[h] = c;
                    size++;
                }
            }
        }
    }

这个方法会对父线程中的线程变量做拷贝 ,其中调用了childValue方法来获取/初始化子线程中的值,并保存到子线程中

  • 由上可见,可继承的线程变量只是 在线程被创建的时候进行了初始化工作,这也就能解释为什么在线程池中发生线程复用时不能获取到父线程线程变量的原因

4. 实验例子流程图

  1. main线程set main线程 变量1时,会调用到InheritableThreadLocalcreateMap方法,创建 inheritableThreadLocals 并保存线程变量
  2. 开启子线程1时,会拷贝父线程中的线程变量到子线程中,如图示
  3. main线程set main线程 变量2,会覆盖主线程中之前set的mian线程变量1
  4. 最后发生线程复用,子线程1无法获取到main线程新set的值,仍然打印 main线程 变量1

5. 解决方案: TransmittableThreadLocal

使用阿里巴巴 TransmittableThreadLocal 能解决线程变量线程封闭 的问题,测试用例如下,在线程池提交任务时调用TtlRunnableget方法来完成线程变量传递

csharp 复制代码
public class TestInheritableThreadLocal {

    private static final TransmittableThreadLocal<String> threadLocal = new TransmittableThreadLocal<>();

    // 固定大小的线程池,保证线程复用
    private static final ExecutorService executorService = Executors.newFixedThreadPool(1);

    public static void main(String[] args) {
        threadLocal.set("main线程 变量1");
        // 正常取到 main线程 变量1
        executorService.execute(() -> System.out.println(threadLocal.get()));

        threadLocal.set("main线程 变量2");
        // 使用TransmittableThreadLocal解决问题
        executorService.execute(TtlRunnable.get(() -> System.out.println(threadLocal.get())));

        executorService.shutdown();
    }
}

输出结果:

main线程 变量1

main线程 变量2


That's all.

相关推荐
uzong6 分钟前
软件架构指南 Software Architecture Guide
后端
又是忙碌的一天6 分钟前
SpringBoot 创建及登录、拦截器
java·spring boot·后端
勇哥java实战分享1 小时前
短信平台 Pro 版本 ,比开源版本更强大
后端
学历真的很重要1 小时前
LangChain V1.0 Context Engineering(上下文工程)详细指南
人工智能·后端·学习·语言模型·面试·职场和发展·langchain
计算机毕设VX:Fegn08951 小时前
计算机毕业设计|基于springboot + vue二手家电管理系统(源码+数据库+文档)
vue.js·spring boot·后端·课程设计
上进小菜猪1 小时前
基于 YOLOv8 的智能杂草检测识别实战 [目标检测完整源码]
后端
韩师傅2 小时前
前端开发消亡史:AI也无法掩盖没有设计创造力的真相
前端·人工智能·后端
栈与堆3 小时前
LeetCode-1-两数之和
java·数据结构·后端·python·算法·leetcode·rust
superman超哥3 小时前
双端迭代器(DoubleEndedIterator):Rust双向遍历的优雅实现
开发语言·后端·rust·双端迭代器·rust双向遍历
1二山似3 小时前
crmeb多商户启动swoole时报‘加密文件丢失’
后端·swoole