写点什么

工作中对 InheritableThreadLocal 使用的思考

  • 2024-12-27
    北京
  • 本文字数:3503 字

    阅读完需:约 11 分钟

作者:京东保险 王奕龙


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

1. 先说结论

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

2. 实验例子

  • 创建一个线程数固定为 1 的线程池,先在 main 线程中存入变量1,并使用线程池开启新的线程打印输出线程变量,之后更改 main 线程的线程变量为变量2,再使用线程池中线程(发生线程复用)打印输出线程变量,对比两次输出的值是否不同


/** * 测试线程池下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 源码

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 的构造方法


    public Thread(ThreadGroup group, String name) {        init(group, null, name, 0);    }
复制代码


  1. 沿着构造方法向下,找到init方法的最终实现,其中有如下逻辑:为当前线程创建线程变量以继承父线程中的线程变量


/** * @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 即为父线程中保存的线程变量


    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方法来完成线程变量传递


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.

发布于: 刚刚阅读数: 5
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
工作中对InheritableThreadLocal使用的思考_京东科技开发者_InfoQ写作社区