专栏首页BAT的乌托邦ThreadLocal垮线程池传递数据解决方案:TransmittableThreadLocal【享学Java】

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

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

前言

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

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


正文

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


InheritableThreadLocal的局限性

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

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;
    }
}

运行程序,控制台打印:

get数据,线程名:pool-1-thread-1,数据为:TestThreadLocal.Person(age=18)
get数据,线程名:pool-1-thread-1,数据为:TestThreadLocal.Person(age=18)

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

private static final ExecutorService THREAD_POOL = Executors.newFixedThreadPool(2);

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

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理解一番。

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可以呢?有了上面的知识储备,现在一探其源码便知:

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:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.11.4</version>
</dependency>

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

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

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

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

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,它能够满足我们对线程间数据传递的几乎一切遐想,这对我们做类似于全链路压测这种平台的时候非常有帮助,期待成效。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 【小家Java】自定义的线程池需要关闭吗?(局部变量Executors线程池一定要手动关闭)

    我之前看到很多同事写代码,为了提高效率,采用多线程去优化。由为了提高多线程的性能,用到了线程池。

    YourBatman
  • 【小家java】Java里的进程、线程、协程 、Thread、守护线程、join线程的总结

    说到线程,很多人最直观的感受就是多线程。本章不讨论高并发、多线程之类的。返璞归真,咱们来讨论讨论线程这个东西到底是什么东西,着眼于线程本身,我们怎么玩? 为了...

    YourBatman
  • 【小家java】Java中主线程(父线程)与子线程的通信和联系

    通俗的讲, 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位(比如QQ是个进程、微信是个进程)

    YourBatman
  • 线程小练习

    抢到锁的线程先执行,没有抢到锁的线程需要等待,等锁用完后需要释放,然后其他等待的线程再去抢这个锁,那个线程抢到那个线程再执行。

    小闫同学啊
  • JUC线程池ThreadPoolExecutor源码分析

    很早之前就打算看一次JUC线程池ThreadPoolExecutor的源码实现,由于近段时间比较忙,一直没有时间整理出源码分析的文章。之前在分析扩展线程池实现可...

    Throwable
  • JUC学习笔记(四)—线程池

    线程池 【死磕Java并发】—–J.U.C之线程池:ThreadPoolExecutor

    Monica2333
  • Java并发编程之线程池

    java.util.concurrent.Executors提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池

    日薪月亿
  • 并发编程之线程池ThreadPoolExecutor

    在我们平时自己写线程的测试demo时,一般都是用new Thread的方式来创建线程。但是,我们知道创建线程对象,就会在内存中开辟空间,而线程中的任务执行完毕之...

    烟雨星空
  • 线程sleep,wait,notify,join,yield方法解析

    调用了 start 方法之后,线程就进入了就绪阶段。此时,线程不会立即执行run方法,需要等待获取CPU资源。

    烟雨星空
  • 浅谈线程和进程

    事实上大部分程序员并没有系统化的学习过,也有很多人并没有机会好好运用它。所以,如果拉一个工作多年的程序员讨论,对方未必能说出个所以然。

    Frank909

扫码关注云+社区

领取腾讯云代金券