线程池如何传递ThreadLocal

神瑛侍者
• 阅读 5942

线程池如何传递ThreadLocal

前言

在做分布式链路追踪系统的时候,需要解决异步调用透传上下文的需求,特别是传递traceId,本文就线程池透传几种方式进行分析。

其他典型场景例子:

  1. 分布式跟踪系统 或 全链路压测(即链路打标)
  2. 日志收集记录系统上下文
  3. SessionCache
  4. 应用容器或上层框架跨应用代码给下层SDK传递信息

1、JDK对跨线程传递ThreadLocal的支持

首先看一个最简单场景,也是一个错误的例子。

    void testThreadLocal(){
        ThreadLocal<Object> threadLocal = new ThreadLocal<>();
        threadLocal.set("not ok");
        new Thread(()->{
            System.out.println(threadLocal.get());
        }).start();
    }

java中的threadlocal,是绑定在线程上的。你在一个线程中set的值,在另外一个线程是拿不到的。

上面的输出是:

null

1.1 InheritableThreadLocal 例子

JDK考虑了这种场景,实现了InheritableThreadLocal ,不要高兴太早,这个只是支持父子线程,线程池会有问题

我们看下InheritableThreadLocal的例子:

        InheritableThreadLocal<String> itl = new InheritableThreadLocal<>();
        itl.set("father");
        new Thread(()->{
            System.out.println("subThread:" + itl.get());
            itl.set("son");
            System.out.println(itl.get());
        }).start();

        Thread.sleep(500);//等待子线程执行完

        System.out.println("thread:" + itl.get());

上面的输出是:

subThread:father //子线程可以拿到父线程的变量

son

thread:father //子线程修改不影响父线程的变量

1.2 InheritableThreadLocal的实现原理

有同学可能想知道InheritableThreadLocal的实现原理,其实特别简单。就是Thread类里面分开记录了ThreadLocal、InheritableThreadLocal的ThreadLocalMap,初始化的时候,会拿到parent.InheritableThreadLocal。直接上代码可以看的很清楚。

class Thread {
    ...
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
  
  ...
  
  if (inheritThreadLocals && parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals =
    ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}

JDKInheritableThreadLocal类可以完成父线程到子线程的值传递。但对于使用线程池等会池化复用线程的执行组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把 任务提交给线程池时ThreadLocal值传递到 任务执行时

2、日志MDC/Opentracing的实现

如果你的应用实现了Opentracing的规范,比如通过skywalking的agent对线程池做了拦截,那么自定义Scope实现类,可以跨线程传递MDC,然后你的义务可以通过设置MDC的值,传递给子线程。

代码如下:

        this.scopeManager = scopeManager;
        this.wrapped = wrapped;
        this.finishOnClose = finishOnClose;
        this.toRestore = (OwlThreadLocalScope)scopeManager.tlsScope.get();
        scopeManager.tlsScope.set(this);
        if (wrapped instanceof JaegerSpan) {
            this.insertMDC(((JaegerSpan)wrapped).context());
        } else if (wrapped instanceof JaegerSpanWrapper) {
            this.insertMDC(((JaegerSpanWrapper)wrapped).getDelegated().context());
        }

3、阿里transmittable-thread-local

github地址:https://github.com/alibaba/tr...

TransmittableThreadLocal(TTL)是框架/中间件缺少的Java™std lib(简单和0依赖),提供了增强的InheritableThreadLocal,即使使用线程池组件也可以在线程之间传输值。

3.1 transmittable-thread-local 官方readme参考:

使用类TransmittableThreadLocal来保存值,并跨线程池传递。

TransmittableThreadLocal继承InheritableThreadLocal,使用方式也类似。相比InheritableThreadLocal,添加了

  1. copy方法
    用于定制 任务提交给线程池时ThreadLocal值传递到 任务执行时 的拷贝行为,缺省传递的是引用。
    注意:如果跨线程传递了对象引用因为不再有线程封闭,与InheritableThreadLocal.childValue一样,使用者/业务逻辑要注意传递对象的线程
  2. protectedbeforeExecute/afterExecute方法
    执行任务(Runnable/Callable)的前/后的生命周期回调,缺省是空操作。

3.2 transmittable-thread-local 代码例子

方式一:TtlRunnable封装:

ExecutorService executorService = Executors.newCachedThreadPool();
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();

// =====================================================
// 在父线程中设置
context.set("value-set-in-parent");

// 额外的处理,生成修饰了的对象ttlRunnable
Runnable ttlRunnable = TtlRunnable.get(() -> {
    System.out.println(context.get());
});
executorService.submit(ttlRunnable);

方式二:ExecutorService封装:

ExecutorService executorService = ...
// 额外的处理,生成修饰了的对象executorService
executorService = TtlExecutors.getTtlExecutorService(executorService);

方式三:使用java agent,无代码入侵

这种方式,实现线程池的传递是透明的,业务代码中没有修饰Runnable或是线程池的代码。即可以做到应用代码 无侵入

ExecutorService executorService = Executors.newCachedThreadPool();
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
// =====================================================
// 在父线程中设置
context.set("value-set-in-parent");

executorService.submit(() -> {
    System.out.println(context.get());
});

4、grpc的实现

grpc是一种分布式调用协议和实现,也封装了一套跨线程传递上下文的实现。

io.grpc.Context 表示上下文,用来在一次grpc请求链路中传递用户登录信息、tracing信息等。

Context常用用法如下。首先获取当前context,这个一般是作为参数传过来的,或通过current()获取当前的已有context。

然后通过attach方法,绑定到当前线程上,并且返回当前线程

    public Runnable wrap(final Runnable r) {
        return new Runnable() {
            @Override
            public void run() {
                Context previous = attach();
                try {
                    r.run();
                } finally {
                    detach(previous);
                }
            }
        };
    }

Context的主要方法如下

  • attach() attach Context自己,从而进入到一个新的scope中,新的scope以此Context实例作为current,并且返回之前的current context
  • detach(Context toDetach) attach()方法的反向方法,退出当前Context并且detach到toDetachContext,每个attach方法要对应一个detach,所以一般通过try finally代码块或wrap模板方法来使用。
  • static storage() 获取storage,Storage是用来attach和detach当前context用的。

线程池传递实现:

ExecutorService executorService = Executors.newCachedThreadPool();
Context.withValue("key","value");

execute(Context.current().wrap(() -> {
            System.out.println(Context.current().getValue("key"));
        }));

5、总结

以上总结的四种实现跨线程传递的方法,最简单的就是自己定义一个Runnable,添加属性传递即可。如果考虑通用型,需要中间件封装一个Executor对象,类似transmittable-thread-local的实现,或者直接使用transmittable-thread-local。

实践的项目中,考虑周全,要支持spanMDCrpc上下文业务自定义上下文,可以参考以上方法封装。

参考资料

[grpc源码分析1-context] https://www.codercto.com/a/66...

[threadlocal变量透传,这些问题你都遇到过吗?]https://cloud.tencent.com/dev...

扫描二维码,关注公众号“猿必过”

线程池如何传递ThreadLocal

回复 “面试题” 自行领取吧。

微信群交流讨论,请添加微信号:zyhui98,备注:面试题加群

本文由猿必过 YBG 发布

禁止未经授权转载,违者依法追究相关法律责任

如需授权可联系:zhuyunhui@yuanbiguo.com

点赞
收藏
评论区
推荐文章
美凌格栋栋酱 美凌格栋栋酱
6个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Stella981 Stella981
3年前
Dubbo日志链路追踪TraceId选型
!mark(https://oscimg.oschina.net/oscnet/updd1ad9729fb807ee6dc473bdc283b1a4481.png)一、目的开发排查系统问题用得最多的手段就是查看系统日志,但是在分布式环境下使用日志定位问题还是比较麻烦,需要借助全链路追踪ID把上下文串联起来,本文主要分享基于
Stella981 Stella981
3年前
Opentracing + Uber Jaeger 全链路灰度调用链,Nepxion Discovery
当网关和服务在实施全链路分布式灰度发布和路由时候,我们需要一款追踪系统来监控网关和服务走的是哪个灰度组,哪个灰度版本,哪个灰度区域,甚至监控从HttpHeader头部全程传递的灰度规则和路由策略。这个功能意义在于:不仅可以监控全链路中基本的调用信息,也可以监控额外的灰度信息,有助于我们判断灰度发布和路由是否执行准确,一旦有问题,也可以快速定位
Stella981 Stella981
3年前
Dubbo + Zipkin + Brave实现全链路追踪
DubboZipkinBrave实现全链路追踪最近写了一个链路追踪Demo分享下,实现了链路追踪过程中数据的记录,还有能扩展的地方,后期再继续补充。原理参考上面文章《Dubbo链路追踪——生成全局ID(traceId)》(https://my.oschina.net/Luc
Wesley13 Wesley13
3年前
Java ThreadLocal的内存泄漏问题
ThreadLocal提供了线程独有的局部变量,可以在整个线程存活的过程中随时取用,极大地方便了一些逻辑的实现。常见的ThreadLocal用法有:\存储单个线程上下文信息。比如存储id等;\使变量线程安全。变量既然成为了每个线程内部的局部变量,自然就不会存在并发问题了;\减少参数传递。比如做一个trace工具,能够输出工程从开始到结
Stella981 Stella981
3年前
Nepxion Discovery Agent
前言基于SpringCloud的全链路灰度蓝绿发布功能,其中一个场景是,基于Header传递的全链路灰度路由,采用配置中心配置路由策略映射在网关或者服务上,支持根据用户自定义Header跟路由策略整合,最终转化为路由Header信息而实现,路由策略传递到全链路服务中。这是一个非常普遍的需求,但如果业务方用了服务之间异步调用的方式,会导致存储在Th
Easter79 Easter79
3年前
TransmittableThreadLocal在使用线程池等会缓存线程的组件情况下传递ThreadLocal
1、简介TransmittableThreadLocal是Alibaba开源的、用于解决“在使用线程池等会缓存线程的组件情况下传递ThreadLocal”问题的InheritableThreadLocal扩展。若希望TransmittableThreadLocal在线程池与主线程间传递,需配合_TtlRunnab
Stella981 Stella981
3年前
Spring Boot + Spring Cloud 构建微服务系统(八):分布式链路追踪(Sleuth、Zipkin)
技术背景在微服务架构中,随着业务发展,系统拆分导致系统调用链路愈发复杂,一个看似简单的前端请求可能最终需要调用很多次后端服务才能完成,那么当整个请求出现问题时,我们很难得知到底是哪个服务出了问题导致的,这时就需要解决一个问题,如何快速定位服务故障点,于是,分布式系统调用链追踪技术就此诞生了。ZipKinZipkin是一个
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Stella981 Stella981
3年前
Noark入门之线程模型
0x00单线程多进程单线程与单进程多线程的目的都是想尽可能的利用CPU,减少CPU的空闲时间,特别是多核环境,今天咱不做深度解读,跳过...0x01线程池锁最早的一部分游戏服务器是采用线程池的方式来处理玩家的业务请求,以达最大限度的利用多核优势来提高处理业务能力。但线程池同时也带来了并发问题,为了解决同一玩家多个业务请求不被
分布式系统中的分布式链路追踪与分布式调用链路
在分布式系统中,由于服务间的调用关系复杂,需要实现分布式链路追踪来跟踪请求在各个服务中的调用路径和时间消耗。这对问题排查和性能监控都很重要。常用的分布式链路追踪实现有基于日志的和基于分布式追踪系统的两种方式: