TransmittableThreadLocal详解

这篇具有很好参考价值的文章主要介绍了TransmittableThreadLocal详解。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

介绍

TransmittableThreadLocal(TTL)是阿里开源的用于解决,在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。
详细的内容可以查看 https://github.com/alibaba/transmittable-thread-local

需要解决的问题

通过上一篇文章我们了解到ThreadLocalInheritable所存在的局限性,针对这种局限性TTL提出并实现了一种解决方案。

如果对于TTL和上面所说的局限性,没有清晰概念的同学可以看下ThreadLocal与InheritableThreadLocal的实现原理和上面TTLgithub上面的介绍

在探究源码之前,我们需要明确使用的场景,以及场景所产生的问题。

  1. 在线程之中建立或使用另一个线程,并且需要继承当前线程的上下文
  2. 建立或使用线程,存在两种情况,第一立即使用,第二某段时间后使用(线程池提交但不马上执行)
  3. 执行任务的线程也存在两种情况,第一新线程(其它线程),第二当前线程(由池化特性决定)

对于第三种情况,以线程池为例来说,如果拒绝策略为CallerRunsPolicy,也就是用提交的线程来执行,那么就存在第二种情况,由当前线程执行

接下来,梳理下可能面临的问题点(以线程池为例)

  1. 何时进行当前线程上下文的获取?
    由于上面的场景存在延时执行,那么获取上下文就只能在新线程创建的时候,对于使用其它线程(线程池存在的线程)就只能是创建任务的时候。

ps:对线程池理解不太明白的同学,可以看看我另一篇文章
ThreadPoolExecutor源码详解

  1. 如何拷贝上下文?
    对于引用对象来说,如果直接使用其地址,可能就存在问题,外层会影响到执行线程的信息,这需要根据业务场景来确定,是否能影响。

  2. 对于当前线程执行的情况,如何保证上下文不丢失?
    这种情况出现在,当我们提交的任务被划分的线程有自己的上下文(任务的提交和实际执行中间存在时间差,如果这个时间段出现了上下文的更新,那么直接覆盖将导致本次更新丢失),那么就需要保证在任务执行的时候是当时的上下文,执行完毕后需要还原。

  3. 什么时候设置上下文?
    由于前面我们知道,在任务提交和执行存在一定的时间差,那么设置上下文的时候,就不能是创建的时候,只能是在执行之前(如果在创建的时候,还需要考虑,中途如果没轮到该任务执行就设置了上下文,线程如果还有其它的流程需要执行,就会导致上下文丢失问题)

最后,我们尝试画下时序图(以线程池为例,最常规的情况)

在对整体流程有了详细理解后,接下来就进行源码阅读

源码

对于如何使用,可以查看官网中的使用方式,使用方式是比较简单的。

TTL整体是通过装饰器模式,来对现有的线程池,Runable进行增强。

最简单的使用方式:

		//使用TTL
		TransmittableThreadLocal<Map<String, Integer>> USER_CONTEXT=new TransmittableThreadLocal<>();
		//将普通的Runable包装成TtlRunnable
		TtlRunnable ttlRunnable = TtlRunnable.get(() -> {
            System.out.println(USER_CONTEXT.get().get("username"));
        });
        new Thread(ttlRunnable).start();

TtlRunnable.get()

根据步骤1提交任务(拷贝上下文),也就是在TtlRunnable.get()方法中,最后就是new TtlRunnable()

    private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
    	//这就是去拷贝当前线程的上下文
        this.capturedRef = new AtomicReference<>(capture());
        this.runnable = runnable;
        this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
    }

这里出现了一个工具类Transmitter,它主要的功能就是帮助我们拷贝,存放,重置ThreadLocal信息,这里ThreadLocal既可能是ThreadLocal也可能是TransmittableThreadLocal,接下来就先看看Transmitter

Transmitter类

其中主要有几个方法需要关注,分别是

  1. 拷贝上下文capture()
  2. 存放上下文replay()
  3. 重置上下文restore()

实际的实现类是Transmittee,保存在

private static final Set<Transmittee<Object, Object>> transmitteeSet = new CopyOnWriteArraySet<>();

总共有两个,一个处理ThreadLocal,一个处理TransmittableThreadLocal
这里就以第二个的拷贝为例,详细代码可以去TransmittableThreadLocal.class中查看:

                    public HashMap<TransmittableThreadLocal<Object>, Object> capture() {
                        final HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value = new HashMap<>(holder.get().size());
                        for (TransmittableThreadLocal<Object> threadLocal : holder.get().keySet()) {
                        	//这里需要注意下,确定copyValue()的拷贝方式
                            ttl2Value.put(threadLocal, threadLocal.copyValue());
                        }
                        return ttl2Value;
                    }

这里的copyValue()方法需要注意下,目前是直接使用value对象,如果是引用对象,那么就会受外层的影响。如果想进行深拷贝,需要使用SuppliedTransmittableThreadLocal类。
通过TransmittableThreadLocal.withInitialAndCopier()方法,提供对应拷贝方法

    private static final class SuppliedTransmittableThreadLocal<T> extends TransmittableThreadLocal<T> {
        private final Supplier<? extends T> supplier;
        private final TtlCopier<T> copierForChildValue;
        private final TtlCopier<T> copierForCopy;

还有一个就是成员变量holder。这个holder中存放了所有TransmittableThreadLocal的引用,而拷贝其实就是将TransmittableThreadLocal的引用和当时其中的值拷贝(取决于拷贝的方式,对于引用类型要考虑是否能受外层影响)到capturedRef成员变量中,这样TtlRunnable就能在运行时获取到上下文了。

下图在总体流程上描述new TtlRunnable()的整个过程
transmittable-thread-local,java,java
到此拷贝已经完成,接下来就是使用前进行值设置。

TtlRunnable.run()

使用前也就是在任务运行前

		//1. 获取快照,也就是Snapshot()
        final Object captured = capturedRef.get();
        if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
            throw new IllegalStateException("TTL value reference is released after run!");
        }
		//2. 将快照中的值设置到当前线程的上下文中(也就是TransmittableThreadLocal或者ThreadLocal)
		//3. 返回backup,就是在设置之前,当前线程的快照信息
        final Object backup = replay(captured);
        try {
            runnable.run();
        } finally {
        	//4.将设置的当前线程快照信息给重新设置回去
            restore(backup);
        }

第一步也就是上面我们刚聊过的,就不过多赘述了。
第二步也就是设置上下文
第三步设置backup

接下来我们来看下replay()方法的源码,以及第三步backup和第四步restore()方法的必要性。

replay()
                    public HashMap<TransmittableThreadLocal<Object>, Object> replay(@NonNull HashMap<TransmittableThreadLocal<Object>, Object> captured) {
                        final HashMap<TransmittableThreadLocal<Object>, Object> backup = new HashMap<>(holder.get().size());

                        for (final Iterator<TransmittableThreadLocal<Object>> iterator = holder.get().keySet().iterator(); iterator.hasNext(); ) {
                            TransmittableThreadLocal<Object> threadLocal = iterator.next();
                            //1.获取当前线程,上线文快照
                            backup.put(threadLocal, threadLocal.get());
                            //2.如果当前线程有快照里面不存在的上下文,那么先清除掉
                            if (!captured.containsKey(threadLocal)) {
                                iterator.remove();
                                threadLocal.superRemove();
                            }
                        }
                        //3.将创建TtlRunnable时保存的快照设置到当前线程的上下文中
                        setTtlValuesTo(captured);
                        //4.保留的一个hook用于自定义
                        doExecuteCallback(true);
                        //5.返回保存的快照
                        return backup;
                    }

对于1,2步骤,主要是把执行时刻的快照保存下来,等执行完后在设置会去,如果有点迷糊可以看下【问题三】
backup解决的场景,提交执行的线程有自己的上下文(场景比较少,但是情况确实存在)

到这里TransmittableThreadLocal大体执行流程就分析完毕,还涉及到的一些方法可以深入源码中去查看下。

觉得不错的同学可以关注下我哟,不定期更新工作中遇到的问题以及解决方案(* ̄︶ ̄)文章来源地址https://www.toymoban.com/news/detail-707740.html

到了这里,关于TransmittableThreadLocal详解的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • JDBC详解(五):批量插入(超详解)

    本博主将用CSDN记录软件开发求学之路上亲身所得与所学的心得与知识,有兴趣的小伙伴可以关注博主!也许一个人独行,可以走的很快,但是一群人结伴而行,才能走的更远! 当需要成批插入或者更新记录时,可以采用Java的批量 更新 机制,这一机制允许多条语句一次性提

    2024年02月02日
    浏览(80)
  • @KafkaListener注解详解(一)| 常用参数详解

    @KafkaListener 注解提供了许多可配置的参数,以便更灵活地定制 Kafka 消息监听器的行为。 描述: 指定监听的 Kafka 主题,可以是一个字符串数组。这是最基本的参数,它定义了监听器将从哪个或哪些主题接收消息。 例子: @KafkaListener(topics = \\\"my-topic\\\") 描述: 指定 Kafka 消费者组

    2024年02月04日
    浏览(40)
  • SQL注入详解(万字文章详解)

    目录 0x01 ⭐前言 0x02 SQL注入原理 0x03 危害 0x04 修复建议 0x05 测试方法 0x06 利用方式   1. 利用 2. 利用SQL注入写文件 3. 利用SQL注入读文件 0x07 SQL 注入分类  1. 联合注入 1.1 判断字段数 1.2 联合查询注入通过 information_schema 获取表 1.3 联合查询注入通过 information_schema 获取字段 1

    2024年02月08日
    浏览(43)
  • Mybatis3详解 之 全局配置文件详解

    前面我们看到的Mybatis全局文件并没有全部列举出来,所以这一章我们来详细的介绍一遍,Mybatis的全局配置文件并不是很复杂,它的所有元素和代码如下所示: 注意:Mybatis的配置文件的顺序是严格按照从上至下的顺序声明,不颠倒顺序,如果颠倒了它们的顺序,那么Mybatis在

    2024年02月07日
    浏览(44)
  • JDBC详解(六):数据库事务(超详解)

    本博主将用CSDN记录软件开发求学之路上亲身所得与所学的心得与知识,有兴趣的小伙伴可以关注博主!也许一个人独行,可以走的很快,但是一群人结伴而行,才能走的更远! 事务:一组逻辑操作单元,使数据从一种状态变换到另一种状态。 事务处理(事务操作): 保证所

    2024年02月04日
    浏览(48)
  • OpenCV SIFT源码详解——detectAndCompute函数详解

    useProvidedKeypoints参数决定当前是探测关键点还是计算描述符 useProvidedKeypoints 为true时,执行compute功能,计算描述符;为false时,执行detect功能,探测关键点; 当探测关键点时,构建的高斯金字塔首层图像会扩大两倍, firstOctave 等于-1;而计算描述符时, firstOctave 会根据传入的

    2024年02月16日
    浏览(40)
  • JDBC详解(四):操作BLOB类型字段(超详解)

    本博主将用CSDN记录软件开发求学之路上亲身所得与所学的心得与知识,有兴趣的小伙伴可以关注博主!也许一个人独行,可以走的很快,但是一群人结伴而行,才能走的更远! ⭕ MySQL中,BLOB是一个二进制大型对象,是一个可以存储大量数据的容器,它能容纳不同大小的数据

    2024年02月04日
    浏览(40)
  • JDBC详解(二):获取数据库连接(超详解)

    本博主将用CSDN记录软件开发求学之路上亲身所得与所学的心得与知识,有兴趣的小伙伴可以关注博主!也许一个人独行,可以走的很快,但是一群人结伴而行,才能走的更远! ⭕ java.sql.Driver 接口是 所有 JDBC 驱动程序需要实现的接口 。这个接口是提供给数据库厂商使用的,

    2023年04月27日
    浏览(43)
  • C语言文件打开关闭详解、文件顺序读写详解。

    fopen 函数原型: const char *filename 文件的路径以及名字 const char *mode 文件的打开方式 文件打开方式 含义 如果文件不存在 “r” 读 文件不存在会报错 “w” 写(清空写) 建立一个新的文件 “a” 写(追加写) 建立一个新的文件 “r+” 读写 文件不存在会报错 “w+” 读写(清空写) 建

    2024年02月10日
    浏览(40)
  • 【详解配置文件系列】es7配置文件详解

    首发博客地址 系列文章地址 cluster.name 是 Elasticsearch 配置文件中的一个重要选项,用于指定集群的名称。每个运行的 Elasticsearch 实例都必须具有相同的集群名称才能加入同一个集群。 cluster.name 是一个自定义的字符串,用于标识一个 Elasticsearch 集群。集群名称可以是任何合法

    2024年02月10日
    浏览(49)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包