从ThreadLocal谈到TransmittableThreadLocal,从使用到原理2

简介: 从ThreadLocal谈到TransmittableThreadLocal,从使用到原理

从ThreadLocal谈到TransmittableThreadLocal,从使用到原理1:https://developer.aliyun.com/article/1394835

局限性

ThreadLocal设计的目的就是为每条线程都开辟一块自己的局部变量存储区域(并不是为了解决线程安全问题设计的,不过使用ThreadLocal可以避免一定的线程安全问题产生),但如果你需要将父线程中的数据共享给子线程时,就不怎么方便啦.

但是这种父线程传递信息给子线程的场景,我们使用的还是不少的,比如使用异步编程时,再或者是下面简单的场景

public class ThreadLocalExample2 {
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    
    public static void main(String[] args) {
        System.out.println("在主线程" + Thread.currentThread().getName() + "中保存临时用户信息");
        String userInfo = "宁在春";
        threadLocal.set(userInfo);
        new Thread(()->{
            // 获取不到父线程存储的信息
            System.out.println("在子线程" + Thread.currentThread().getName() + "中获取临时用户信息 " + threadLocal.get());
        },"MyThread2").start();
        threadLocal.remove();
    }
}
// 在主线程main中保存临时用户信息
// 在子线程pool-1-thread-1中获取临时用户信息 null

从输出结果中可以看到,子线程是无法获取到的,这是因为threadLocals就是存储在当前线程中而已。

然后就又有了InheritableThreadLocal的出现,继续吧。

注意事项

在继续往下之前,谈一个注意点,很多时候还会谈到ThreadLocal的副作用,脏数据和内存泄漏问题,但较真起来,这个问题更多的是开发时产生的问题。在每次使用 ThreadLocal 时,一定要记得在结束前及时调用 remove()方法清理数据

InheritableThreadLocal

很多时候,我们可能需要在线程中获取到父线程存储的相关信息,比如我们上面谈的那个简单例子,现在我们换使用InheritableThreadLocal看看可行不。

案例

public class InheritableThreadLocalExample {
    private static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
    public static void main(String[] args) {
        System.out.println("在主线程" + Thread.currentThread().getName() + "中保存临时用户信息");
        String userInfo = "宁在春";
        threadLocal.set(userInfo);
        new Thread(()->{
            // 获取不到父线程存储的信息
            System.out.println("在子线程" + Thread.currentThread().getName() + "中获取临时用户信息 " + threadLocal.get());
        },"MyThread2").start();
        threadLocal.remove();
    }
}
//输出:
//在主线程main中保存临时用户信息
//在子线程MyThread2中获取临时用户信息 宁在春

怎么实现父子线程传值的?

要知道怎么实现的,无疑还是要去看createMap、set和get方法,看看它做了些什么改动。

之前我在类图中也说到了,Thread 中有这ThreadLocalMap threadLocals 和 inheritableThreadLocal 两个私有变量。

ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

InheritableThreadLocal 是继承了ThreadLocal,并重写和实现了其中些许方法。

   private static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

源码非常非常简短,或者说主要的逻辑还是在 Thread 和 ThreadLocal 中~

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    // 1、获取父线程的数据
    protected T childValue(T parentValue) {
        return parentValue;
    }
    // 2、 获取 inheritableThreadLocals 变量
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
    // 3、为当前线程进行 inheritableThreadLocals 的初始化
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

在这个方法里面,只有T childValue()是我们在 ThreadLocal 中没有接触过的方法,那么肯定是有点妙用的。其他的就是从threadLocals改成了inheritableThreadLocals,没有太多改变。

真正的起点是在new Thread(() - >{})这段代码中,相信很多人,包括我在此之前都没有怎么看过Thread的构造函数过程

public Thread(Runnable target, String name) {
    init(null, target, name, 0);
}
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize) {
    init(g, target, name, stackSize, null, true);
}
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    }
    this.name = name;
    // 获取当前执行线程作为父线程
    Thread parent = currentThread();
    SecurityManager security = System.getSecurityManager();
    // 省略一些检查相关的代码...
    // 将当前执行线程设置为创建出的线程的父线程
    this.group = g;
    // 省略了一些不是关注点的代码...
    
    // 我们需要关注的点
    // 判断 父线程的inheritThreadLocals 和 当前线程的 inheritThreadLocals 是否为 null
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        // 不为null,才进行初始化,设置子线程中的inheritableThreadLocals变量
        this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    // 为创建出的线程分配默认线程栈大小
    this.stackSize = stackSize;
    // 设置线程ID
    tid = nextThreadID();
}

说起来,我们要关注的就是下面这一句

this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
// 传递的参数是:当前线程的inheritableThreadLocals变量
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}
//  ThreadLocalMap类 私有构造函数
private ThreadLocalMap(ThreadLocalMap parentMap) {
    // 获取父线程中的存储的所有变量
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];
    // 循环复制父线程中的Entry
    for (int j = 0; j < len; j++) {
        Entry e = parentTable[j];
        if (e != null) {
            @SuppressWarnings("unchecked")
            ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
            if (key != null) {
                //调用了 InheritableThreadLocal 重写的 childValue 方法
                // 获取到 e.value 值
                Object value = key.childValue(e.value);
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

小结

所以InheritableThreadLocal本质上就是通过复制来实现父子线程之间的传值

this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);完这一段代码后,子线程就已经存储了父线程的所有Entry信息了。

局限性:

InheritableThreadLocal 支持子线程访问父线程,本质上就是在创建线程的时候将父线程中的本地变量值全部复制到子线程中。

但是在谈到并发时,不可避免的会谈到线程池,因为线程的频繁创建和销毁,对于程序来说,代价实在太大。

而在线程池中,线程是复用的,并不用每次新建,那么此时InheritableThreadLocal复制的父线程就变成了第一个执行任务的线程了,即后面所有新建的线程,他们所访问的本地变量都源于第一个执行任务的线程(期间也可能会遭遇到其他线程的修改),从而造成本地变量混乱

比如:

假如我们有这样的一个流程,10个请求到达controller,然后调用service,在service中我们还要执行一个异步任务,最后等待结果的返回。

10个service - > 10个异步任务 ,在service,我们会设置一个变量副本,在执行异步任务的子线程中,需要get出来进行调用。

public class InheritableThreadLocalDemo3 {
    /**
     * 业务线程池,service 中执行异步任务的线程池
     */
    private static ExecutorService businessExecutors = Executors.newFixedThreadPool(5);
    /**
     * 线程上下文环境,在service中设置环境变量,
     * 然后在这里提交一个异步任务,模拟在子线程(执行异步任务的线程)中,是否可以访问到刚设置的环境变量值。
     */
    private static InheritableThreadLocal<Integer> requestIdThreadLocal = new InheritableThreadLocal<>();
    public static void main(String[] args) {
        // 模式10个请求,每个请求执行ControlThread的逻辑,其具体实现就是,先输出父线程的名称,
        for (int i = 0; i < 10; i++) {
            // 然后设置本地环境变量,并将父线程名称传入到子线程中,在子线程中尝试获取在父线程中的设置的环境变量
            new Thread(new ServiceThread(i)).start();
        }
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //关闭线程池
        businessExecutors.shutdown();
    }
    /**
     * 模拟Service业务代码
     */
    static class ServiceThread implements Runnable {
        private int i;
        public ServiceThread(int i) {
            this.i = i;
        }
        @Override
        public void run() {
            requestIdThreadLocal.set(i);
            System.out.println("执行service方法==>在"+Thread.currentThread().getName() + "中存储变量副本==>" + i);
            // 异步编程 CompletableFuture.runAsync()创建无返回值的简单异步任务,businessExecutors 表示线程池~
            CompletableFuture<Void> runAsync = CompletableFuture.runAsync(() -> {
                try {
                    // 模拟执行时间
                    Thread.sleep(500L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:"+requestIdThreadLocal.get());
            }, businessExecutors);
            requestIdThreadLocal.remove();
        }
    }
}

输出:

执行service方法==>在Thread-0中存储变量副本==>0
执行service方法==>在Thread-1中存储变量副本==>1
执行service方法==>在Thread-6中存储变量副本==>6
执行service方法==>在Thread-3中存储变量副本==>3
执行service方法==>在Thread-4中存储变量副本==>4
执行service方法==>在Thread-5中存储变量副本==>5
执行service方法==>在Thread-2中存储变量副本==>2
执行service方法==>在Thread-7中存储变量副本==>7
执行service方法==>在Thread-8中存储变量副本==>8
执行service方法==>在Thread-9中存储变量副本==>9
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:1
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:7
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:9
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:2
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:5
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:7
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:9
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:2
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:5
执行异步任务,在执行异步任务的线程中,获取父线程(service)中存储的值:1

可以看到在子线程中获取到的变量值已经重复~ 此时线程变量副本值已经错乱啦。

然后接着就出现了TransmittableThreadLocal啦,接着看吧,看看他们是怎么解决的

从ThreadLocal谈到TransmittableThreadLocal,从使用到原理3:https://developer.aliyun.com/article/1394838

目录
相关文章
|
存储 Java 测试技术
【通用行业开发部】阿里开源TransmittableThreadLocal使用经验记录
本文章主要记录我在一次业务优化中,使用线程池带来的子父线程值传递问题,及使用TransmittableThreadLocal来解决该问题的经验,并对TransmittableThreadLocal原理做些梳理。
|
缓存 Java 应用服务中间件
一文带你使用xxl-job定时任务
将调度行为抽象形成“调度中心”公共平台,而平台自身并不承担业务逻辑,“调度中心”负责发起调度请求。 将任务抽象成分散的JobHandler,交由“执行器”统一管理,“执行器”负责接收调度请求并执行对应的JobHandler中业务逻辑。 因此,“调度”和“任务”两部分可以相互解耦,提高系统整体稳定性和扩展性;
4318 0
一文带你使用xxl-job定时任务
|
设计模式 缓存 Java
从ThreadLocal谈到TransmittableThreadLocal,从使用到原理3
从ThreadLocal谈到TransmittableThreadLocal,从使用到原理
2071 1
|
10月前
|
消息中间件 存储 Java
吃透 RocketMQ 消息中间件,看这篇就够了!
本文详细介绍 RocketMQ 的五大要点、核心特性及应用场景,涵盖高并发业务场景下的消息中间件关键知识点。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
吃透 RocketMQ 消息中间件,看这篇就够了!
|
11月前
|
存储 Java 测试技术
一文彻底搞懂阿里开源TransmittableThreadLocal的原理和使用
【10月更文挑战第2天】在Java多线程编程中,线程本地变量(ThreadLocal)是一个非常有用的工具,它能够在每个线程中保存一个独立的变量副本,从而避免多线程环境下的数据竞争问题。然而,在使用线程池等高级多线程技术时,ThreadLocal却面临着一些挑战。为了解决这个问题,阿里巴巴开源了TransmittableThreadLocal(TTL),它扩展了ThreadLocal的功能,使其能够在复杂的多线程环境中正确传递值。本文将深入探讨TTL的原理和使用,帮助读者彻底理解这一技术干货。
1658 0
|
Java 关系型数据库 数据库连接
MyBatis-Plus介绍及Spring Boot 3集成指南
MyBatis-Plus是一个MyBatis扩展工具,旨在简化Java开发中的CRUD操作。它具有无侵入性、低损耗、强大的CRUD功能、Lambda表达式支持、主键自动生成、ActiveRecord模式、全局操作和内置代码生成器等特点。在Spring Boot 3中集成MyBatis-Plus,需在pom.xml添加依赖,排除特定版本的mybatis-spring,并用@MapperScan注解指定Mapper接口路径。此外,还介绍了如何使用MyBatis-Plus代码生成器自动生成Mapper、Model、Service和Controller层代码,以加速开发。
2000 2
MyBatis-Plus介绍及Spring Boot 3集成指南
|
存储 前端开发 Java
从ThreadLocal谈到TransmittableThreadLocal,从使用到原理1
从ThreadLocal谈到TransmittableThreadLocal,从使用到原理
3579 0
|
存储 前端开发 Java
深入剖析ThreadLocal使用场景、实现原理、设计思想
深入剖析ThreadLocal使用场景、实现原理、设计思想
深入剖析ThreadLocal使用场景、实现原理、设计思想
|
消息中间件 测试技术 领域建模
DDD - 一文读懂DDD领域驱动设计
DDD - 一文读懂DDD领域驱动设计
37953 5
|
监控 Dubbo Java
超详细的Sentinel入门
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
超详细的Sentinel入门