• 工作中对InheritableThreadLocal使用的思考


    大家好,我是方圆。最近在工作中结合线程池使用 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()));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    输出结果:


    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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

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

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

    在这里插入图片描述

    3.2.1 childValue方法
    1. 开启新线程时,会调用Thread的构造方法
        public Thread(ThreadGroup group, String name) {
            init(group, null, name, 0);
        }
    
    • 1
    • 2
    • 3
    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    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++;
                    }
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    这个方法会对父线程中的线程变量做拷贝,其中调用了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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    输出结果:
    main线程 变量1
    main线程 变量2

    • 注意:对象类型需要注意线程安全问题
    • 具体用法参考 https://github.com/alibaba/transmittable-thread-local

    That’s all.

  • 相关阅读:
    从零开始搭建自己的cli脚手架
    Spark项目实战-卡口流量统计
    【sql】You can‘t specify target table for update in FROM clause
    NLP学习:深入NLP
    Docker在边缘计算中的崭露头角:探索容器技术如何驱动边缘计算的新浪潮
    jumpserver如何录入web资产
    JavaSE - 方法
    上周热点回顾(6.5-6.11)
    C# WPF 开发一个 Emoji 表情查看软件
    [Qualcomm][Voice Call]语音通话流程和问题分析
  • 原文地址:https://blog.csdn.net/qq_46225886/article/details/127859791