从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

目录
相关文章
|
4月前
|
Java 数据库连接 调度
xxljob执行源码分析
本文深入解析XXL-JOB分布式任务调度框架源码,涵盖架构设计、核心执行流程与关键线程池机制。内容包括任务触发、注册、失败重试、日志报告及时间轮调度原理,结合带中文注释的源码包与分析导图,全面剖析其高性能设计实现。
 xxljob执行源码分析
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
Redis分布式锁在高并发场景下是重要的技术手段,但其实现过程中常遇到五大深坑:**原子性问题**、**连接耗尽问题**、**锁过期问题**、**锁失效问题**以及**锁分段问题**。这些问题不仅影响系统的稳定性和性能,还可能导致数据不一致。尼恩在实际项目中总结了这些坑,并提供了详细的解决方案,包括使用Lua脚本保证原子性、设置合理的锁过期时间和使用看门狗机制、以及通过锁分段提升性能。这些经验和技巧对面试和实际开发都有很大帮助,值得深入学习和实践。
太惨痛: Redis 分布式锁 5个大坑,又大又深, 如何才能 避开 ?
|
缓存 NoSQL Java
RedisTemplate操作Redis,这一篇文章就够了
redis是一款开源的Key-Value数据库,运行在内存中,由C语言编写。企业开发通常采用Redis来实现缓存。同类的产品还有memcache 、memcached 等。
3509 1
|
设计模式 缓存 Java
从ThreadLocal谈到TransmittableThreadLocal,从使用到原理3
从ThreadLocal谈到TransmittableThreadLocal,从使用到原理
2987 1
|
消息中间件 存储 负载均衡
2024消息队列“四大天王”:Rabbit、Rocket、Kafka、Pulsar巅峰对决
本文对比了 RabbitMQ、RocketMQ、Kafka 和 Pulsar 四种消息队列系统,涵盖架构、性能、可用性和适用场景。RabbitMQ 以灵活路由和可靠性著称;RocketMQ 支持高可用和顺序消息;Kafka 专为高吞吐量和低延迟设计;Pulsar 提供多租户支持和高可扩展性。性能方面,吞吐量从高到低依次为
6492 1
|
XML Java 数据格式
使用完全注解的方式进行AOP功能实现(@Aspect+@Configuration+@EnableAspectJAutoProxy+@ComponentScan)
本文介绍了如何使用Spring框架的注解方式实现AOP(面向切面编程)。当目标对象没有实现接口时,Spring会自动采用CGLIB库进行动态代理。文中详细解释了常用的AOP注解,如`@Aspect`、`@Pointcut`、`@Before`等,并提供了完整的示例代码,包括业务逻辑类`User`、配置类`SpringConfiguration`、切面类`LoggingAspect`以及测试类`TestAnnotationConfig`。通过这些示例,展示了如何在方法执行前后添加日志记录等切面逻辑。
1495 2
使用完全注解的方式进行AOP功能实现(@Aspect+@Configuration+@EnableAspectJAutoProxy+@ComponentScan)
|
存储 前端开发 Java
从ThreadLocal谈到TransmittableThreadLocal,从使用到原理1
从ThreadLocal谈到TransmittableThreadLocal,从使用到原理
4077 0
|
监控 Dubbo Java
超详细的Sentinel入门
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
超详细的Sentinel入门
|
存储 Java 测试技术
一文彻底搞懂阿里开源TransmittableThreadLocal的原理和使用
【10月更文挑战第2天】在Java多线程编程中,线程本地变量(ThreadLocal)是一个非常有用的工具,它能够在每个线程中保存一个独立的变量副本,从而避免多线程环境下的数据竞争问题。然而,在使用线程池等高级多线程技术时,ThreadLocal却面临着一些挑战。为了解决这个问题,阿里巴巴开源了TransmittableThreadLocal(TTL),它扩展了ThreadLocal的功能,使其能够在复杂的多线程环境中正确传递值。本文将深入探讨TTL的原理和使用,帮助读者彻底理解这一技术干货。
2814 0

热门文章

最新文章

下一篇
开通oss服务