前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >被同组实习生吓一跳,他竟然知道InheritableThreadLocal和TransmittableThreadLocal

被同组实习生吓一跳,他竟然知道InheritableThreadLocal和TransmittableThreadLocal

作者头像
程序员牛肉
发布2024-11-04 18:21:26
980
发布2024-11-04 18:21:26
举报
文章被收录于专栏:小牛肉带你学Java

大家好,我是程序员牛肉。

今天在水群的时候,一个群友抛出了一个技术问题:

简单来讲就是说有一个父线程要搞异步操作。但是这个异步操作用的线程是线程池里面的线程。并且在这一过程中,父线程要向这些子线程传递信息

我顺嘴问了一句和我一起实习的同学。没想到他很快的就答出来用TransmittableThreadLocal。

确实是狠狠震惊我了,没想到这么长的英文他都能念对。并且他竟然还能说出来为什么不能使用InheritableThreadLocal。

那么什么是InheritableThreadLocal和TransmittableThreadLocal呢?这两个类又有什么区别呢?

先提一嘴ThreadLocal吧,避免有的朋友忘记了。

[ThreadLocal 是 Java 中的一个类,它提供了线程局部变量。这些变量是线程私有的,每个使用该变量的线程都有其自己的独立初始化副本,多个线程之间不会共享这个变量。ThreadLocal 通常用于隔离线程间的数据,以避免共享状态带来的同步问题。]

说白了这个类可以让每一个线程都往其中存储独属于当前线程的变量。现在请看下面这个例子:

代码语言:javascript
复制
public class ThreadLocalExample {
    public static void main(String[] args) throws InterruptedException {

        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        threadLocal.set(Thread.currentThread().getName());
        ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.execute(() -> {

            System.out.println("子线程的threadloacl值为"+Thread.currentThread().getName() + " : " + threadLocal.get());
        });
        Thread.sleep(10);
        System.out.println("主线程的threadloacl值为"+Thread.currentThread().getName() + " : " + threadLocal.get());
        executor.shutdown();
    }
}

执行结果为:

这个例子其实反应了一个问题:ThreadLocal是没有办法跨线程传递数值的。那我们如何才能让子线程继承父线程的ThreadLocal变量呢?

正所谓有需求就有市场。为了解决这个问题,InheritableThreadLocal被设计了出来,可以实现跨线程传递ThreadLocal变量。

基于InheritableThreadLocal就可以改写上面那个例子为:

代码语言:javascript
复制
public class InheritableThreadLocalExample {
    public static void main(String[] args) throws InterruptedException {
        InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
        inheritableThreadLocal.set("主线程的threadlocal值为"+Thread.currentThread().getName());
        Thread childThread = new Thread(() -> {
            System.out.println("子线程的threadlocal值为"+Thread.currentThread().getName() + " : " + inheritableThreadLocal.get());
        });

        Thread.sleep(10);
        System.out.println(Thread.currentThread().getName() + " : " + inheritableThreadLocal.get());
    }
}

执行结果为:

太吓人了,那InheritableThreadLocal这个变量到底是如何实现子线程获取父线程的InheritableThreadLocal值的?

先不着急看源码,让我们用正常思路来想一想这是怎么实现的。

如果我们想让子线程获取到父线程的InheritableThreadLocal值,最简单的思路其实就是直接把父线程的InheritableThreadLocal值赋值给子线程。

那么应该什么时候赋值呢?

仔细一想其实没得选,我们只能在子线程初始化的时候尝试完成这段逻辑。让我们看一看源码是不是这样实现的:

在线程的初始化方法中,我们可以看到这样一段代码:

太吓人了,这段代码逻辑不就是当父线程存在InheritableThreadLocal值的时候,我们把当前线程的InheritableThreadLocal值设置为父线程对应的值嘛?

没想到实现逻辑还真和我们推理的一样。那么此时其实就会出现一个问题:如果我们对父线程的InheritableThreadLocal重新赋值之后,子线程的InheritableThreadLocal会变化吗?

让我们试验一下:

代码语言:javascript
复制
public class InheritableThreadLocalExample {
    public static void main(String[] args) {
        InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
        inheritableThreadLocal.set(Thread.currentThread().getName());
        
        ExecutorService executor = Executors.newFixedThreadPool(20);

        // 提交20个任务到线程池
        for (int i = 0; i < 20; i++) {
            executor.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " : " + inheritableThreadLocal.get());
            });
        }
        inheritableThreadLocal.set("修改当前的主线程变量");

        for (int i = 0; i < 20; i++) {
            executor.execute(() -> {

                System.out.println(Thread.currentThread().getName() + " : " + inheritableThreadLocal.get());
            });
        }
        executor.shutdown();
    }
}

在这段代码中,我们尝试修改主线程中的InheritableThreadLocal值,我们看看子线程的输出会有变化吗?

我们可以发现InheritableThreadLocal重新赋值之后对于子线程不生效。原因是因为init方法只会在线程被创建的时候创建一次,而线程池却会一直复用这些线程。

[在这里要说明一下:我在看网上很多文档的时候,会发现一些博主说InheritableThreadLocal在线程池中用不了。

但我想说:不是用不了。而是只有在线程的初始化阶段,父线程的InheritableThreadLocal值才会被同步到子线程的InheritableThreadLocal中。但是线程池的线程只会被初始化一次。也就是说线程池中的InheritableThreadLocal相当于只会被设置一次。]

这问题我们都能发现,计算机发展了这么多年肯定也就把这个问题解决了。现在比较知名的是阿里巴巴开源的TransmittableThreadLocal。

https://github.com/alibaba/transmittable-thread-local 对应的GitHub仓库

先不看源码,直接上来就搞代码轰炸没人能看得懂。所以我们先自己推一推要如何实现这个功能。

我们先想一想,父线程和线程池中的线程交互的时间点并不多。但是有一个时间点大家一定都知道:提交任务的时候。

当我们想要给线程池提供一个任务的时候,就需要在父线程中先构建好任务类(Runnable 和Callable)。我们完全可以在构造任务的时候,尝试把父线程的本地缓存传递给子线程。

没错,这其实就是TransmittableThreadLocal一部分的设计原理。为什么说是一部分呢?其实作者在issue中已经解答过了这个问题:

作者的回答是:

我也是看了好几天才理解了作者的意思,所以我在这里解释一下为什么不能仅仅使用修饰Runnable或者Callbable:

  • 单一上下文传递:提到的代理Runnable方式适用于只传递一个上下文的场景。如果需要传递多个上下文,这种方式就需要为每个上下文创建一个代理类,这会导致代码复杂度增加。
  • 透明性问题:业务方可能不需要关心上下文的传递细节,他们只是希望上下文能够在不同线程间正确传递。代理Runnable的方式需要业务方显式地使用代理类,这降低了透明性。
  • 嵌套执行问题:在没有线程切换的情况下(例如,使用ThreadPoolExecutor.CallerRunsPolicy),上下文的设置和清理会变得复杂。如果一个Runnable在执行另一个Runnable之前没有正确清理上下文,那么后续的逻辑可能会使用错误的上下文。

[CallerRunsPolicy是一种线程池策略,CallerRunsPolicy 策略既不会抛弃任务,也不会抛出异常,而是将任务回退到调用者线程(即提交任务的线程)中执行。如果执行程序已关闭,则会丢弃该任务]

因此作者在TransmittableThreadLocal设计出了以下三种方法:

  • Capture(捕获):在上下文需要被传递之前,捕获当前线程的上下文信息。
  • Replay(重放):在目标线程中重放捕获的上下文信息,确保目标线程能够访问到正确的上下文。
  • Restore(恢复):在上下文被传递之后,恢复原始线程的上下文,以避免对其他任务产生影响。

现在让我们来看一看源码是对Runnable和Callable是怎么处理的:

在这段方法中,我们可以看到对Runnalbe的run方法的修饰也基本就是基于上述提到的三个方法实现的。所以这三种方法在代码中到底是怎么样体现的呢?

Capture方法:

Capture调用了三个方法,分别是Shapshot,captureValues,captureThreadLocalValues,因此看一看这三个方法:

captureValues:

这个方法遍历 TransmittableThreadLocal.holder 中存储的所有 TransmittableThreadLocal 实例,并为每个实例调用 copyValue() 方法来获取其当前值的副本。这些实例和它们的值被存储在一个 HashMap 中,这个 HashMap 就是 captureTtlValues() 方法的返回值。

CaptureThreadLocalValues:

这个方法遍历所有注册的 ThreadLocal 实例(通过 Transmitter.registerThreadLocal() 方法注册),并为每个实例调用其 TtlCopier 的 copy() 方法来获取其当前值的副本。这些实例和它们的值也被存储在一个 HashMap 中,这个 HashMap 就是 captureThreadLocalValues() 方法的返回值。

Shapshot:

Snapshot 是 Transmitter 类的一个私有静态内部类,它用于存储上下文信息的快照。它包含两个 HashMap 字段,一个用于存储 TransmittableThreadLocal 的值,另一个用于存储 ThreadLocal 的值。

也就是说Capture方法是在搞Snapshot,用来存储当前线程的上下文信息。

Replay方法:

这个方法没什么好讲的,其实就是获取到当前线程的shapshot。在Run方法中我们用这种方式来传递父子线程的本地变量。

我们着重看一下这个方法内部调用的一个叫做replayTtlValues的方法:

继续看这个setTtlValuesTo方法:

for循环了一波父线程的快照传递给子线程。在这一个过程中,如果子线程本来就有ttl的话就会污染。因此我们在外层的replayTtlValue中当子线程出现一个父线程没有的ttl后,就要把这个ttl删除了。

Restore方法:

restore 方法通过使用之前捕获的快照(Snapshot)来恢复原始线程的上下文信息。这个过程确保了在任务执行完毕后,原始线程的上下文能够被正确地恢复,从而避免了对其他任务的潜在影响。

还记得之前我们自己提出的设计思路嘛?

其实我们的设计思路已经和源码很相似了。只不过源码是赋值snapshot,并且在使用完之后还要对恢复线程池中线程自己的上下文。也就是说被修饰后的Runnbale的流程是:

  • 拿到父线程本地变量拷贝
  • 赋值给当前线程,并且保存好当前线程之前就有的本地变量
  • 执行任务代码代码逻辑
  • 复原子线程之前本来就有的本地变量

最后再来看一看TransmittableThreadLocal中的get和set方法:

让我们点进去看一看addTishHolder这个方法:

我们继续看一看holder这个变量:

代码语言:javascript
复制
    private static final InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder = new InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>() {
        protected WeakHashMap<TransmittableThreadLocal<Object>, ?> initialValue() {
            return new WeakHashMap();
        }

        protected WeakHashMap<TransmittableThreadLocal<Object>, ?> childValue(WeakHashMap<TransmittableThreadLocal<Object>, ?> parentValue) {
            return new WeakHashMap(parentValue);
        }    };

这个holder的作用就是:保存当前线程所有的threadlocal用于在TtlRunnable.get(task)生成快照。

所以其实我们在搞快照的时候,就是从这个holder中搞的:

关于TransmittableThreadLocal的介绍就到这里了,阿里的这个设计确实很复杂。阿里这个TTl是对原有的threadlocal进行做snapshot,然后执行之前进行快照回放。逻辑写的很晦涩难通。

因此我这篇文章对于TransmittableThreadLocal的介绍感觉也没有做到醍醐灌顶。等我后续再读一读我们公司的TransmissableThreadLocal之后,看看是否能够对这种设计有更加透彻的理解。

对于TransmittableThreadLocal,你有什么想说的嘛?欢迎在评论区留言。

关注我,带你了解更多计算机干货。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-11-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序员牛肉 微信公众号,前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档