首页 > 其他分享 >工作中对InheritableThreadLocal使用的思考

工作中对InheritableThreadLocal使用的思考

时间:2024-12-27 16:54:06浏览次数:4  
标签:变量 thread threadLocal 线程 思考 使用 InheritableThreadLocal main

作者:京东保险 王奕龙

代码评审时,发现在线程池中使用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);
    }

2. 沿着构造方法向下,找到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);
    
    ...
    
}

3. 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.

标签:变量,thread,threadLocal,线程,思考,使用,InheritableThreadLocal,main
From: https://www.cnblogs.com/Jcloud/p/18636170

相关文章

  • 使用Nuclia Understanding API处理非结构化数据的实战指南
    大家好,今天我们来聊聊NucliaUnderstandingAPI,这是一个能够自动索引你从各种内部和外部来源获取的非结构化数据的神器。不管是视频、音频转录,还是图片内容提取、文档解析,Nuclia都能搞定。这波操作可以说是相当丝滑。技术背景介绍我们在处理非结构化数据时,总是面临着如何......
  • Thinkphp 使用workerman消息实现消息推送完整示例
    1.版本说明:ThinkPHP6.0+、PHP8.0+2.实现原理    应用场景,监听用户登陆,修改用户登陆状态,通过心跳时间计算用户是否下线,其他应用场景自行修改逻辑即可。第一步:安装workermancomposerrequiretopthink/think-worker第二步:修改配置文件//执行以上代码后,config目录......
  • 毕设自救DAY1 Markdown 的使用
    Markdown标题:一级标题:#(一个井号空格+标题)二级标题:#再加一个#+标题....一直到六级标题字体:hello在文字两边加星号变斜体hello三个星号斜体加粗hello文字两边两个星号加粗hello文字两边两个波浪号划掉引用:一个箭头符号>做引用效果笑口常开,好彩自然来!分割线:三......
  • 使用 CloudFlare CDN网站遇到「重定向的次数过多」解决方案
    在使用CloudFlareCDN的时候,一开始没有问题,但是过了一段时间,发现网站打不开了,网页提示:重定向次数过多解决办法进入cloudflare管理面板,在左侧点击SSL/TLS菜单,在右侧点击Configure配置按钮。在CustomSSL/TLS中选择Full(Strict)选项进行保存即可。......
  • ES elasticsearch-8.17.0-windows-x86_64使用
    1、下载ES(elasticsearch简称ES下载链接:https://www.elastic.co/downloads/elasticsearch)ES下载链接:https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-8.17.0-windows-x86_64.zip)),需要在修改ES配置,进入ES解压以后的文件下的config目录,找到elasticsearc......
  • 如何声明、实例化和使用委托(C# 编程指南)
    Learn  .NET  C# 使用英语阅读保存    如何声明、实例化和使用委托(C#编程指南)项目2023/04/0714个参与者反馈本文内容示例可靠编程另请参阅可以使用以下任一方法声明委托:使用匹配签名声明委托类型并声明方法:C#复制 //Decl......
  • 阿里云镜像服务使用指南
    阿里云容器镜像服务ACR(AlibabaCloudContainerRegistry)是面向容器镜像、HelmChart等符合OCI标准云原生制品安全托管及高效分发平台。ACR企业版支持全球同步加速、大规模和大镜像分发加速、多代码源构建加速等全链路加速能力,与容器服务ACK无缝集成,帮助企业降低交付复杂度,......
  • 在牧区使用4G太阳能无线视频监控需要注意什么?
    在辽阔的牧区,太阳能无线监控系统的应用为牧民们带来了极大的便利,不仅提升了牧场的安全管理水平,还有效保护了生态环境和牲畜安全。然而,在牧区这一特殊环境中使用太阳能无线监控系统,也需要注意一系列问题,以确保系统的稳定运行和监控效果。那么在实际应用中,在牧区使用太阳能无线监......
  • ISUP协议视频平台EasyCVR小知识:双网口录像机有哪些作用?如何使用
    在现代视频监控系统中,双网口录像机(NVR)因其灵活性和功能性而越来越受到重视。这种设备通过提供两个独立的网络接口,能够满足多种复杂的网络环境需求,特别是在需要连接不同网络或隔离网络环境的项目中。本文将详细介绍双网口录像机的概念、作用以及其在实际项目中的应用,特别是多址设......
  • 工作坊报名|使用 TEN Framework 与 Azure,探索你的多模态交互新场景
    活动背景GPT-4oRealtimeAPI发布,语音AI正在进入一场新的爆发。AI的实时语音和视觉互动能力将为我们带来更多全新创意和应用场景。实时音频交互:允许应用程序实时接收并响应语音和文本输入。自然语音生成:减少AI语音的机械感,使对话更加人性化。多语言能力:促进多语言之间......