前言
在做分布式链路追踪零碎的时候,须要解决异步调用透传上下文的需要,特地是传递traceId,本文就线程池透传几种形式进行剖析。
其余典型场景例子:
- 分布式跟踪零碎 或 全链路压测(即链路打标)
- 日志收集记录零碎上下文
Session
级Cache
- 利用容器或下层框架跨利用代码给上层
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);}
JDK
的InheritableThreadLocal
类能够实现父线程到子线程的值传递。但对于应用线程池等会池化复用线程的执行组件的状况,线程由线程池创立好,并且线程是池化起来重复应用的;这时父子线程关系的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
,增加了
copy
办法
用于定制 工作提交给线程池时 的ThreadLocal
值传递到 工作执行时 的拷贝行为,缺省传递的是援用。
留神:如果跨线程传递了对象援用因为不再有线程关闭,与InheritableThreadLocal.childValue
一样,使用者/业务逻辑要留神传递对象的线程protected
的beforeExecute
/afterExecute
办法
执行工作(Runnable
/Callable
)的前/后的生命周期回调,缺省是空操作。
3.2 transmittable-thread-local 代码例子
形式一:TtlRunnable封装:
ExecutorService executorService = Executors.newCachedThreadPool();TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();// =====================================================// 在父线程中设置context.set("value-set-in-parent");// 额定的解决,生成润饰了的对象ttlRunnableRunnable ttlRunnable = TtlRunnable.get(() -> { System.out.println(context.get());});executorService.submit(ttlRunnable);
形式二:ExecutorService封装:
ExecutorService executorService = ...// 额定的解决,生成润饰了的对象executorServiceexecutorService = 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。
实际的我的项目中,思考周全,要反对span、MDC、rpc上下文、业务自定义上下文,能够参考以上办法封装。
参考资料
[grpc源码剖析1-context] https://www.codercto.com/a/66...
[threadlocal变量透传,这些问题你都遇到过吗?]https://cloud.tencent.com/dev...
扫描二维码,关注公众号“猿必过”
回复 “面试题” 自行支付吧。
微信群交换探讨,请增加微信号:zyhui98,备注:面试题加群
本文由猿必过 YBG 公布禁止未经受权转载,违者依法追究相干法律责任
如需受权可分割:zhuyunhui@yuanbiguo.com