关于java:线程池如何传递ThreadLocal

62次阅读

共计 4987 个字符,预计需要花费 13 分钟才能阅读完成。

前言

在做分布式链路追踪零碎的时候,须要解决异步调用透传上下文的需要,特地是传递 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…

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

回复“面试题”自行支付吧。

微信群交换探讨,请增加微信号:zyhui98,备注:面试题加群

本文由猿必过 YBG 公布

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

如需受权可分割:zhuyunhui@yuanbiguo.com

正文完
 0