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。



相关文章
|
23小时前
|
SQL Java
java处理数据查看范围
java处理数据查看范围
|
1天前
|
Java 调度
Java并发编程:深入理解线程池
【5月更文挑战第11天】本文将深入探讨Java中的线程池,包括其基本概念、工作原理以及如何使用。我们将通过实例来解释线程池的优点,如提高性能和资源利用率,以及如何避免常见的并发问题。我们还将讨论Java中线程池的实现,包括Executor框架和ThreadPoolExecutor类,并展示如何创建和管理线程池。最后,我们将讨论线程池的一些高级特性,如任务调度、线程优先级和异常处理。
|
1天前
|
存储 Java 数据库连接
Java中文乱码浅析解决方案
Java中文乱码浅析解决方案
8 0
|
1天前
|
安全 Java
【JAVA进阶篇教学】第十篇:Java中线程安全、锁讲解
【JAVA进阶篇教学】第十篇:Java中线程安全、锁讲解
|
1天前
|
安全 Java
【JAVA进阶篇教学】第六篇:Java线程中状态
【JAVA进阶篇教学】第六篇:Java线程中状态
|
1天前
|
缓存 Java
【JAVA进阶篇教学】第五篇:Java多线程编程
【JAVA进阶篇教学】第五篇:Java多线程编程
|
1天前
|
Java
【JAVA基础篇教学】第十二篇:Java中多线程编程
【JAVA基础篇教学】第十二篇:Java中多线程编程
|
1天前
|
安全 Java
java-多线程学习记录
java-多线程学习记录
|
2天前
|
Java
【Java多线程】面试常考 —— JUC(java.util.concurrent) 的常见类
【Java多线程】面试常考 —— JUC(java.util.concurrent) 的常见类
12 0
|
2天前
|
设计模式 消息中间件 安全
【Java多线程】关于多线程的一些案例 —— 单例模式中的饿汉模式和懒汉模式以及阻塞队列
【Java多线程】关于多线程的一些案例 —— 单例模式中的饿汉模式和懒汉模式以及阻塞队列
9 0