前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >ThreadLocal垮线程池传递数据解决方案:TransmittableThreadLocal【享学Java】

ThreadLocal垮线程池传递数据解决方案:TransmittableThreadLocal【享学Java】

作者头像
YourBatman
发布2020-03-18 20:55:36
4.1K0
发布2020-03-18 20:55:36
举报
文章被收录于专栏:BAT的乌托邦BAT的乌托邦

企业不是慈善机构:创造利润是你存在的核心价值。

前言

上篇文章 了解到了,ThreadLocal它并不能解决线程安全问题,它旨在用于传递数据。但是它能成功传递数据比如有个大前提:放数据和取数据的操作必须是处于相同线程

即使JDK扩展出了一个子类:InheritableThreadLocal,它能够支持跨线程传递数据,但也仅限于父线程给子线程来传递数据。倘若两个线程间真的八竿子打不着,比如分别位于两个线程池内的线程,它们之间要传递数据该肿么办呢?这就是跨线程池之间的数据传递范畴,是本文将要讲解的主要内容。


正文

在实际生产中,线程一般不可能孤立的独立去运行,而是交给线程池去调度处理。所以实际上几乎没有纯正的父子线程的关系存在,而若有这种需求大多是线程池与线程池之间的线程联系。


InheritableThreadLocal的局限性

上篇文章 介绍了ThreadLocal的局限性,可以使用更强的子类InheritableThreadLocal予以解决。那么这里看看如下示例:

代码语言:javascript
复制
public class TestThreadLocal {

    private static final ThreadLocal<Person> THREAD_LOCAL = new InheritableThreadLocal<>();
    private static final ExecutorService THREAD_POOL = Executors.newSingleThreadExecutor();

    @Test
    public void fun1() throws InterruptedException {
        THREAD_LOCAL.set(new Person());


        THREAD_POOL.execute(() -> getAndPrintData());
        TimeUnit.SECONDS.sleep(2);
        Person newPerson = new Person();
        newPerson.setAge(100);
        THREAD_LOCAL.set(newPerson); // 给线程重新绑定值


        THREAD_POOL.execute(() -> getAndPrintData());
        TimeUnit.SECONDS.sleep(2);
    }


    private void setData(Person person) {
        System.out.println("set数据,线程名:" + Thread.currentThread().getName());
        THREAD_LOCAL.set(person);
    }

    private Person getAndPrintData() {
        Person person = THREAD_LOCAL.get();
        System.out.println("get数据,线程名:" + Thread.currentThread().getName() + ",数据为:" + person);
        return person;
    }

    @Setter
    @ToString
    private static class Person {
        private Integer age = 18;
    }
}

运行程序,控制台打印:

代码语言:javascript
复制
get数据,线程名:pool-1-thread-1,数据为:TestThreadLocal.Person(age=18)
get数据,线程名:pool-1-thread-1,数据为:TestThreadLocal.Person(age=18)

重新绑定竟然“未生效”?在原基础上什么都不动,仅仅只改变线程池的大小:

代码语言:javascript
复制
private static final ExecutorService THREAD_POOL = Executors.newFixedThreadPool(2);

再次运行程序,控制台打印:

代码语言:javascript
复制
get数据,线程名:pool-1-thread-1,数据为:TestThreadLocal.Person(age=18)
get数据,线程名:pool-1-thread-2,数据为:TestThreadLocal.Person(age=100)

这个结果能接受且符合预期。可以看到线程名是不一样的,所以第二个线程获取到了最新绑定的结果。因此可以大胆猜测:线程在init初始化的时候,才会去同步一份最新数据过来

对于这两个示例的结果可做如下解释:

  • 示例1的线程池大小是1,所以第二个线程执行时复用的是上个线程(你看线程名称都一样),所以就不会再经历init初始化阶段,所以得到的绑定数据还是旧数据
  • 示例2的线程池大小是2,所以第二个线程执行时会继续初始化一条新的线程来执行它,会触发到init过程,所以它获取到的是最新绑定的数据。

小提示:线程池内线程数量若还没达到coreSize大小的话,每次新任务都会启用新的线程来执行的(不管是否有空闲线程与否)


Thread#init方法探究

为了理解后面方案的实现,非常有必要对线程初始化方法Thread#init理解一番。

代码语言:javascript
复制
Thread#init:

	// inheritThreadLocals是否继承线程的本地变量们(默认是true)
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        ...
		Thread parent = currentThread();
		...
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
		...
        /* Set thread ID */ // 给线程一个自增的id
        tid = nextThreadID();
	}

子线程是通过在父线程中通过调用new Thread()方法来创建子线程Thread#init方法在Thread的构造方法中被调用。

从摘录出来的源码出能得到如下重点:

  1. 当前线程作为新创建线程(子线程)的父线程
  2. 如果父线程绑定了变量(inheritableThreadLocals != null)并且允许继承(inheritThreadLocals = true),那么就会把父线程绑定的变量们 拷贝一份到子线程里
    1. 拷贝的原理类似于Map复制,只不过其在Hash冲突时,不是使用链表结构,而是直接在数组中找下一个为null的槽位放里面

说明:这里的拷贝是浅拷贝:引用传递而已。如果想要深度拷贝,需要自行复写ThreadLocal#childValue()方法(比如你可以继承InheritableThreadLocal并重写childValue方法)

那么为何ThreadLocal不具备继承性,而InheritableThreadLocal可以呢?有了上面的知识储备,现在一探其源码便知:

代码语言:javascript
复制
public class InheritableThreadLocal<T> extends ThreadLocal<T> {

	// 现在知道为何是浅拷贝了吧~~~~~~
    protected T childValue(T parentValue) {
        return parentValue;
    }

    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
    // 只要inheritableThreadLocals不为null了,那可不就完成子线程可以继承父的了吗
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }

}

源码不会骗人,一切都透露得明明白白的了吧。


InheritableThreadLocal支持子线程访问父线程中本地变量的原理是:创建子线程时将父线程中的本地变量值拷贝了一份到自己这来,拷贝的时机是子线程创建时

然后在实际开发中,多线程就离不开线程池的使用,因为线程池能够复用线程,减少线程的频繁创建与销毁。倘若合格时候使用InheritableThreadLocal来传递数据,那么线程池中的线程拷贝的数据始终来自于第一个提交任务的外部线程,这样非常容易造成线程本地变量混乱,这种错误是致命的,比如示例1就是这种例子~

那么,这种问题怎么破?JDK并没有提供源生的支持,这时候就得借助阿里巴巴开源的TTL(transmittable-thread-local):TransmittableThreadLocal


TransmittableThreadLocal

TTL是阿里巴巴开源的专门解决InheritableThreadLocal的局限性,实现线程本地变量在线程池的执行过程中,能正常的访问父线程设置的线程变量。

TransmittableThreadLocal简称TTL,InheritableThreadLocal简称ITL

它的官网是:https://github.com/alibaba/transmittable-thread-local 功能介绍我已截图至此:

在这里插入图片描述
在这里插入图片描述

GAV:

代码语言:javascript
复制
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.11.4</version>
</dependency>

那么使用它就能解决如上示例的问题吗?正所谓试验是检验真理的唯一标准,来一把:

针对示例1,仅仅做出如下改动(其它均不变):

代码语言:javascript
复制
// 实现类使用TTL的实现
private static final ThreadLocal<Person> THREAD_LOCAL = new TransmittableThreadLocal<>();
// 线程池使用TTL包装一把
private static final ExecutorService THREAD_POOL = TtlExecutors.getTtlExecutorService(Executors.newSingleThreadExecutor());

再次运行程序,控制台打印:

代码语言:javascript
复制
get数据,线程名:pool-1-thread-1,数据为:TestThreadLocal.Person(age=18)
get数据,线程名:pool-1-thread-1,数据为:TestThreadLocal.Person(age=100)

bingo!看线程名仍旧还是同一个线程(因为线程池大小为1嘛),但是结果已经是最新的了,这才是合理的嘛,不禁想感叹一句:太它xxxxx了!

说明:这里线程池必须使用TtlExecutors处理一下,而且得使用TransmittableThreadLocal作为数据传递的实现,缺一不可哦~


如何实现?

TransmittableThreadLocal继承于InheritableThreadLocal,并拥有了 InheritableThreadLocal对子线程传递上下文的特性,只需解决线程池上下文传递问题。它使用TtlRunnable包装了任务的运行,被包装的run方法执行异步任务之前,会使用replay进行设置父线程里的本地变量给当前子线程,任务执行完毕,会调用restore恢复该子线程原生的本地变量,当然重点还是稍显复杂的上下文管理部分。

本文并不涉及到它详细的原理,建议有兴趣者可以上它官网看看(不算很复杂),全中文的也好理解,并且还附有其执行时序图。

说明:它还支持javaagent完全零侵入方式接入,可以说是非常强大和好用的一个基础工具,值得使用明白,对中间件团队能提供良好的支持。


使用场景

官方流出了其四大使用场景:

  1. 分布式跟踪系统(链路追踪)
  2. 日志收集记录系统上下文(MDC)
  3. Session级Cache
  4. 应用容器或上层框架跨应用代码给下层SDK传递信息

其中场景1和场景2在全链路压测平台打造的时候都会触及到,所以基于TTL来解决这些问题不失外一个非常好的选择。


总结

ThreadLocal的一步步的进化,最终来到了TransmittableThreadLocal,它能够满足我们对线程间数据传递的几乎一切遐想,这对我们做类似于全链路压测这种平台的时候非常有帮助,期待成效。

分隔线
分隔线

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 正文
    • InheritableThreadLocal的局限性
      • Thread#init方法探究
    • TransmittableThreadLocal
      • 如何实现?
      • 使用场景
  • 总结
相关产品与服务
消息队列 TDMQ
消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档