TransmittableThreadLocal 原理分析

news2024/11/24 13:10:54

前言

注:在看此篇文章前,你需要了解 ThreadLocal、InheritableThreadLocal 的原理。


这里先总体的介绍TransmittableThreadLocal (下文以 ttl 作为简称)的原理再去分析一些核心的源码,旨在先有个整体的认识,再去详细了解源码。

整体介绍

复制父线程的 ttl 到 TtlRunnable

由于InheritableThreadLocal是新建线程时复制父线程的本地变量到子线程,在线程池中由于线程只创建一次,因此无法复制新增的本地变量。
ttl 照着这个思路去找替代方案,在新建线程任务(Runnable)时将这些本地变量放到另一个类的属性字段中保存,这个类就是 TtlRunnable
下图的 holder 是一个 InheritableThreadLocal,存的是当前线程的一个 WeakHashMap key 为 ttl 对象 value 为 null,在这里可以暂时先忽略,后续再详细介绍。
复制父线程 ttl

将 TtlRunnable 保存的 ttl 复制到子线程

在执行 TtlRunnablerun() 方法时,再将属性中保存的值通过 TransmittableThreadLocal#set() 方法复制到子线程的 holder 中。需要注意的是 set() 方法并不只复制到 holder,还调用 super.set() 方法,这样相当于在子线程执行一次 ThreadLocal.set(),那么子线程就复制了父线程的本地变量。
子线程 ttl

子线程如何访问值

通过上一步,已完成从父线程将 ttl 复制到子线程,那就跟 InheritableThreadLocal 类似了,通过 get() 方法就能拿到当前子线程在父类中同一个 ttl 对象对应的值。

小结

父线程通过在新建线程任务时,将父线程所有的 ttl 先保存到 TtlRunnable 对象中,在运行任务时,再将 TtlRunnable 中的值再复制到子线程中,这样子线程就能够访问父线程中同一个 ttl 对象对应的值;这里的话如果不明白没关系,也是先给个整体的框架先了解下,后面再细分析一波就懂了。
需要注意的是,上面的步骤没有提到子线程 holder 的备份与恢复。

详细分析

我们以下面这个例子进行分析,下面代码演示了 TransmittableThreadLocal 的一种基本用法:

public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(10);
    TransmittableThreadLocal<String> ttl = new TransmittableThreadLocal<>();
    // 父线程设置值
    ttl.set("法外狂徒张三");
    // 需要使用 TtlRunnable 包装线程任务 Runnable,完成复制
    Runnable ttlRunnable = TtlRunnable.get(() -> {
        // 获取同一个 ttl 对象在父子线程中的值是否一致
        String value = ttl.get();
        System.out.println(value);
    });

    executorService.submit(ttlRunnable);
    executorService.shutdown();
}

现在先介绍下刚才说到的 holder:

private static final InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> 
holder =
        new InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>() {
            @Override
            protected WeakHashMap<TransmittableThreadLocal<Object>, ?> initialValue() {
                return new WeakHashMap<TransmittableThreadLocal<Object>, Object>();
            }

            @Override
            protected WeakHashMap<TransmittableThreadLocal<Object>, ?> childValue(WeakHashMap<TransmittableThreadLocal<Object>, ?> parentValue) {
                return new WeakHashMap<TransmittableThreadLocal<Object>, Object>(parentValue);
            }
        };

从上面源码可以看到 holder 是一个静态私有的 InheritableThreadLocal,存储的内容是 WeakHashMap,因为 Java 没有提供WeakHashSet,所以用这个替代,而这个“Set”存的就是文章开头图的 ttl1ttl2ttl3等;实现的两个方法其实就是避免在 get() 的时候空指针,从下面这段代码可以看出:

if (!holder.get().containsKey(this)) {
    holder.get().put((TransmittableThreadLocal<Object>) this, null); // WeakHashMap supports null value.
}

正菜开始

  1. 在执行 ttl.set("法外狂徒张三"); 时,做了哪些?点进源码 (父线程中执行)
public final void set(T value) {
    // ...

	// 调用父类设置值,本质还是与 ThreadLocal 一致
    super.set(value);
    // 将当前 this 对象添加到 holder,为后续复制给子线程做准备
    addThisToHolder();
    
    // ...
}

// ....

private void addThisToHolder() {
	// 先判断下,避免直接 put 消耗性能
    if (!holder.get().containsKey(this)) {
    	// 当 Set 使用,所以 value 为 null
        holder.get().put((TransmittableThreadLocal<Object>) this, null); // WeakHashMap supports null value.
    }
}

这一步其实就做两件事,第一将调用 ThreadLocal#set() 方法设置本地变量值;第二将 ttl 添加到 holder 中

  1. 再往下走,执行到 TtlRunnable.get(...); 这一步就开始复制父线程的 ttl 对象到 TtlRunnable(父线程中执行)
public final class TtlRunnable implements Runnable, TtlWrapper<Runnable>, TtlEnhanced, TtlAttachments {
	// 父线程复制的 ttl 就存在这里
    private final AtomicReference<Object> capturedRef;
    // 线程任务
    private final Runnable runnable;
    
    // 略...

    private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
    	// 通过调用 capture() 完成父线程本地变量的获取
        this.capturedRef = new AtomicReference<Object>(capture());
        this.runnable = runnable;
        
        // 略...
    }
    
    // 略...
}

注意这里复制到 TtlRunnable 还是在父线程这里执行。通过 Transmitter#capture() 方法完成复制,并放到原子类中,至于为什么用原子类不是本文的重点,只要知道是为了保证在运行 run() 方法时保证线程安全就行。
下面我们看看 capture() 的具体实现:

public static class Transmitter {
	// 略...
    public static Object capture() {
        return new Snapshot(captureTtlValues(), captureThreadLocalValues());
    }

	// 复制父线程 ttl 的值
    private static HashMap<TransmittableThreadLocal<Object>, Object> captureTtlValues() {
        HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value = new HashMap<TransmittableThreadLocal<Object>, Object>();
        // 由于当前方法在父线程执行,直接通过 holder 可以拿到父线程的所有 ttl
        for (TransmittableThreadLocal<Object> threadLocal : holder.get().keySet()) {
            ttl2Value.put(threadLocal, threadLocal.copyValue());
        }
        return ttl2Value;
    }

	// 复制父线程 threadlocal 的值
    private static HashMap<ThreadLocal<Object>, Object> captureThreadLocalValues() {
        final HashMap<ThreadLocal<Object>, Object> threadLocal2Value = new HashMap<ThreadLocal<Object>, Object>();
        for (Map.Entry<ThreadLocal<Object>, TtlCopier<Object>> entry : threadLocalHolder.entrySet()) {
            final ThreadLocal<Object> threadLocal = entry.getKey();
            final TtlCopier<Object> copier = entry.getValue();

            threadLocal2Value.put(threadLocal, copier.copy(threadLocal.get()));
        }
        return threadLocal2Value;
    }
}

// 存储父线程的 本地变量 值使用,就两个 Map
private static class Snapshot {
    final HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value;
    final HashMap<ThreadLocal<Object>, Object> threadLocal2Value;

    private Snapshot(HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value, HashMap<ThreadLocal<Object>, Object> threadLocal2Value) {
        this.ttl2Value = ttl2Value;
        this.threadLocal2Value = threadLocal2Value;
    }
}

经过上面的步骤,就完成了父线程复制到 TrlRunnable 保存。

  1. 再继续执行 executorService.submit(ttlRunnable); 提交任务到线程池,开始执行TrlRunnable 的 run() 方法
public void run() {
	// 获取父线程保存的本地变量,这里为 Snapshot 对象
    final Object captured = capturedRef.get();
    if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
        throw new IllegalStateException("TTL value reference is released after run!");
    }
	// * 这一步骤最重要,这里完成了备份跟设置本地变量到子线程中
    final Object backup = replay(captured);
    try {
        runnable.run();
    } finally {
    	// 最后恢复备份的值
        restore(backup);
    }
}

关于备份恢复,这里不做介绍有兴趣可以自己看源码,这里就着重讲下如何复制到子线程的

public static Object replay(@NonNull Object captured) {
    final Snapshot capturedSnapshot = (Snapshot) captured;
    return new Snapshot(replayTtlValues(capturedSnapshot.ttl2Value), replayThreadLocalValues(capturedSnapshot.threadLocal2Value));
}

private static HashMap<TransmittableThreadLocal<Object>, Object> replayTtlValues(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> captured) {
    HashMap<TransmittableThreadLocal<Object>, Object> backup = new HashMap<TransmittableThreadLocal<Object>, Object>();
	// 因为现在在子线程里,所以这个 holder 是子线程原来的本地变量,需要备份它们
    for (final Iterator<TransmittableThreadLocal<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {
        TransmittableThreadLocal<Object> threadLocal = iterator.next();

        // backup
        backup.put(threadLocal, threadLocal.get());

        // clear the TTL values that is not in captured
        // avoid the extra TTL values after replay when run task
        if (!captured.containsKey(threadLocal)) {
            iterator.remove();
            threadLocal.superRemove();
        }
    }

    // 重点看这步,将父线程的值复制到子线程
    setTtlValuesTo(captured);

    // call beforeExecute callback
    doExecuteCallback(true);

    return backup;
}

 private static void setTtlValuesTo(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> ttlValues) {
 	// 到这里,你可能忘了这个 ttlValues 是哪里来的,这里再说明下
 	// 这个值是从父线程的 holder 里面复制出来的,并存放到 TtlRunnable 对象中
 	// 这里对应的是总体介绍的第二步中的图
    for (Map.Entry<TransmittableThreadLocal<Object>, Object> entry : ttlValues.entrySet()) {
        TransmittableThreadLocal<Object> threadLocal = entry.getKey();
        // 这里明确下,set 是跟线程挂钩的,其实底层是放在线程的一个 Map 集合中
        // key 就是 threadLocal,value 就是要设置的值。
        // 这一步就表示,在子线程也设置一个与父线程使用同一个 key 的本地变量
        // 那么在子线程通过这个 key 就能拿到父线程一样的值,这里的 key 就是 ttl 对象
        threadLocal.set(entry.getValue());
    }
}
  1. 最后在子线程通过 String value = ttl.get(); 就能拿到父线程的值了
public final T get() {
	// 因为 set 的时候也是保存在线程的 threadlocalMap 中,直接通过 super.get() 就能拿到值
    T value = super.get();
    if (disableIgnoreNullValueSemantics || null != value) addThisToHolder();
    return value;
}

总结

本文一开始总体的介绍了下 TransmittableThreadLocal 的原理,先了解个大概的流程,后面再通过实际的源码分析进一步说明是如何复制的。但这里由于篇幅有限,并没有分析其他除复制外的内容,感兴趣的读者可以自己去加以分析。
如有任何错误,欢迎指出!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/766284.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

(学习笔记-连接断开)TCP四次挥手

TCP四次挥手过程 TCP断开连接是通过四次挥手实现的&#xff0c;双方都可以主动断开连接&#xff0c;断开连接后主机中的资源将被释放&#xff0c;四次挥手的过程如下&#xff1a; 客户端打算关闭连接时&#xff0c;会发送一个TCP首部FIN标志位为1的报文&#xff0c;也就是FIN报…

预警先行,问题零失控,提升物流的重要利器

每一次大促活动&#xff0c;都是商家们的大卖良机&#xff0c;然而出单之后&#xff0c;最怕出现发货异常的问题。比如包裹长时间未揽收、物流长时间未更新...稍有不慎就会影响店铺权重&#xff0c;甚至深陷各种取消订单、退款赔偿的泥潭。 这时物流监控预警就显得格外重要了。…

浏览器书签栏的小图标设置

在我们写项目中肯定需要自定义这些浏览器的图标 , 那么如何设置呢 <link rel"icon" href"favicon.png" type"image/x-icon" /> 其中的href是选择路径 像vue中 , 基本上都是在文件夹public中的index.html设置浏览器标题跟图标 , 图片的大小…

数据库数据恢复-Oracle数据库文件有坏块损坏的数据恢复案例

Oracle数据库故障&检测&#xff1a; 打开oracle数据库报错&#xff1a;“system01.dbf需要更多的恢复来保持一致性&#xff0c;数据库无法打开”。 北亚企安数据恢复工程师检测数据库文件发现sysaux01.dbf有坏块&#xff0c;sysaux01.dbf文件损坏。数据库无备份&#xff0c…

Windows10环境下安装Kibnana

Windows10环境下安装Kibnana 一、Kibana 介绍1. 数据可视化&#xff1a;2. 仪表板&#xff1a;3. 查询和过滤&#xff1a;4. 地理信息系统&#xff08;GIS&#xff09;支持&#xff1a;5. 实时监控和警报&#xff1a; 二、安装步骤1. 官网地址&#xff1a;2. 选择操作系统3. 解…

echarts——柱状图+折线图

var myChart echarts.init(document.getElementById(myChart)); var option {title: {text: XX增速,textStyle: {color: #2bffff,fontSize: 14,fontWeight: 100,fontFamily: "fontStyle"},left: 0,top: 0,},tooltip: {show: true,backgroundColor: rgba(38,39,40,0…

Waves 14 Complete for Mac(Waves混音效果全套插件)

Waves 14 Complete for Mac是一款音频插件套装&#xff0c;拥有多种不同的音频处理插件、高品质音效、简单易用的界面、完全兼容和兼容多平台等特点&#xff0c;可以帮助音频制作人员进行音频处理和混音&#xff0c;提高音频制作的效率和质量。 音乐创作是一个永不停歇的探索过…

Apache Doris (三十):Doris 数据导入(八)Spark Load 3- 导入HDFS数据

目录 1. 准备HDFS数据 2. 创建Doris表 3. 创建Spark Load导入任务 4. 查看导入任务状态 进入正文之前&#xff0c;欢迎订阅专题、对博文点赞、评论、收藏&#xff0c;关注IT贫道&#xff0c;获取高质量博客内容&#xff01; 宝子们订阅、点赞、收藏不迷路&#xff01;抓紧…

【iOS】—— 属性关键字及weak关键字底层原理

文章目录 先来看看常用的属性关键字有哪些&#xff1a;内存管理有关的的关键字&#xff1a;&#xff08;weak&#xff0c;assign&#xff0c;strong&#xff0c;retain&#xff0c;copy&#xff09;关键字weak关键字assignweak 和 assign 的区别&#xff1a;关键字strong&#…

时间序列的季节性:3种模式及8种建模方法

分析和处理季节性是时间序列分析中的一个关键工作&#xff0c;在本文中我们将描述三种类型的季节性以及常见的8种建模方法。 什么是季节性? 季节性是构成时间序列的关键因素之一&#xff0c;是指在一段时间内以相似强度重复的系统运动。 季节变化可以由各种因素引起&#xf…

看见未来:定位咨询如何预测行业趋势

商业竞争时代&#xff0c;变化无处不在。科技日新月异&#xff0c;消费者需求日益多元&#xff0c;市场环境更加动态不定。在这个快速发展的时代&#xff0c;如果企业想要继续领先&#xff0c;就必须有能力预见未来&#xff0c;适应并驾驭这些变化&#xff0c;这就是定位咨询的…

【QT】使用QtCreator进行debug

使用QtCreator进行debug 简单操作 设置断点 鼠标右键在需要打断点的地方打断点 debug 点击小甲虫按钮&#xff0c;启动之后可以看到三个窗口表格 这里显示变量的值。 这里显示函数当前的执行处以及断点的地方 这三个按钮分别代表的含义是&#xff1a; step into: 单步执行&…

通过宝塔面板部署一个SpringBoot+Vue前后端分离项目的指南(三更)

采取的部署方案 阿里云服务器->FinalShell->宝塔面板。 近期需要将自己的一个SpringBootVue前后端分离项目&#xff0c;并且是分模块开发的项目部署到服务器上&#xff0c;记录一下踩坑的地方&#xff0c;结合C站大佬的解决方案&#xff0c;循循善诱一步步部署到服务器上…

部署开源项目 Casdoor 身份认证管理系统到本地

前言 Casdoor是一个基于OAuth 2.0、OIDC、SAML 和 CAS 的&#xff0c;UI-first的身份和访问管理(IAM)/单点登录(SSO)平台。使用 Go 和react开发&#xff0c;前后端分离&#xff0c;内置第三方应用登录服务。 Casdoor 有四个核心概念&#xff0c;分别是 组织(Organization)&am…

vue3+antd——项目搭建初始化配置——技能提升

vue-antd-admin vue2 版本链接&#xff1a; https://gitee.com/iczer/vue-antd-admin?_fromgitee_search vue3 版本链接&#xff1a; https://github.com/stepui/stepin-template 预览地址: https://stepui.gitee.io/stepin-template 使用文档: http://stepui.gitee.io/step…

全网最全,接口测试面试题+答案,轻松拿捏面试官...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 你怎么理解get和p…

5. 缓存模块

缓存概述 对于缓存功能&#xff0c;相信大家都十分熟悉了。一旦我们发现系统的性能存在瓶颈需要优化时&#xff0c;可能第一时间想到的方式就是加缓存。缓存本质上是一种空间换时间的技术&#xff0c;它将计算结果保存在距离用户更近、或访问效率更高的存储介质中&#xff0c;…

使用supervisor启动进程open files too many问题

今天线上出现了open files too many的问题&#xff0c;查看问题&#xff1a; 1. ulimit -a查看系统最大值发现可以开启的文件句柄只有1024个 果断修复&#xff1a; 1. 查看全局配置文件 ls /etc/security/limits.d/ 比如环境中有如下配置文件&#xff0c;20-nproc.conf名字可…

e2e测试框架之Cypress

谈起web自动化测试&#xff0c;大家首先想到的是Selenium&#xff01;随着近几年前端技术的发展&#xff0c;出现了不少前端测试框架&#xff0c;这些测试框架大多并不依赖于Selenium&#xff0c;这一点跟后端测试框架有很大不同&#xff0c;如Robot Framework做Web自动化测试本…

linux -rw-r--r-x的含义

-rw-r--r-x的含义 权限显示位一共为10位&#xff0c;分为四段&#xff0c;从第2位算起&#xff0c;每3个1组 -rw-r--r-x-表示为普通文件文件所属用户拥有的权限rw-&#xff1a;426该用户所属组拥有的权限r--&#xff1a;4其他用户拥有的权限r-x&#xff1a;415 操作英文对应数…