背景
新的工作新的开始,先描述下问题的背景,项目中为了解决多数据源聚合快速响应问题,启用线程池并发调用多数据源服务获取数据,做聚合接口对外输出,同时也带来了问题,日志跟踪需要跟踪线程池服务调用及数据处理,为了不影响原有的方法参数列表,采用ThreadLocal进行了日志链路追踪,有时候会产生根据ThreadLocal设置的traceId线程池执行后无法快速定位单个服务调用所产生的日志,只能通过上下文去人肉排查,对于程序员是件很苦逼的事情。感谢儿子今天借我用一下他的电脑...
调研步骤:
1.ThreadLocal 为什么没有能传递traceId?
2.jdk本身是否有适应这种场景的线程变量去处理父子线程的问题?
3.是否有开源框架已经解决了这样的问题?
4.总结
1.TheadLocal为啥没能传递traceId?
这个问题很好解释,ThreadLocal本身是线程的内部变量,隶属于线程本身,不能跨线程传输数据。
2.jdk本身是否有适应这种场景的线程变量去处理父子线程的问题?
Thread.class中提供了另外一个变量InheritableThreadLocal,通过分析Thread源码可看到如下代码片段:
Thread类中声明了一个ThreadLocalMap 变量
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
在子线程创建时,会将父线程的inheritableThreadLocals赋值到子线程中。
/**
* Initializes a Thread.
*
* @param g the Thread group
* @param target the object whose run() method gets called
* @param name the name of the new Thread
* @param stackSize the desired stack size for the new thread, or
* zero to indicate that this parameter is to be ignored.
* @param acc the AccessControlContext to inherit, or
* AccessController.getContext() if null
* @param inheritThreadLocals if {@code true}, inherit initial values for
* inheritable thread-locals from the constructing thread
*/
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
.........
/**
* 此处为父子线程在初始化线程时赋值的过程
*/
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
........
/* Set thread ID */
tid = nextThreadID();
}
代码块第二个地方Thread#init方法,说明只能在父线程创建子线程时,能够实现父子线程之间通过threadLocal传值。如果像线程池这种有可能复用线程的情形,则会出现无法传递的问题。到此发现问题可能没有想象的简单。
3.是否有开源框架已经解决了这样的问题?
既然问题这么明显,是否有前辈已经解决了呢,如果有的话是否有热心大神开源了,找了一下果然找到了大神的真迹。transmittable-thread-local 阿里开源
TransmittableThreadLocal是阿里开源的库,继承了InheritableThreadLocal,优化了在使用线程池等会池化复用线程的情况下传递ThreadLocal的使用。
简单来说,有个专门的TtlRunnable和TtlCallable包装类,用于读取原Thread的ThreadLocal对象及值并存于Runnable/Callable中,在执行run或者call方法的时候再将存于Runnable/Callable中的ThreadLocal对象和值读取出来,存入调用run或者call的线程中。
TransmittableThreadLocal 调用时序如下:
TransmittableThreadLocal的通过修饰线程池的使用方式
省去每次Runnable
和Callable
传入线程池时的修饰,这个逻辑可以在线程池中完成。通过工具类com.alibaba.ttl.threadpool.TtlExecutors
完成,有下面的方法:
-
getTtlExecutor
:修饰接口Executor
-
getTtlExecutorService
:修饰接口ExecutorService
-
getTtlScheduledExecutorService
:修饰接口ScheduledExecutorService
使用Java Agent植入修饰代码
Java Agent(Instrumentation)是JDK1.5引入的技术,基于JVM TI机制,使得开发者可以构建一个独立于应用程序的代理(Agent),用来监测和协助运行在 JVM 上的程序,以及替换和修改某些类的定义。开发者可以在一个普通 Java 程序运行时,通过 – javaagent 参数指定一个特定的 jar 文件(包含 Instrumentation 代理)来启动相应的代理程序,植入自己扩展的修饰代码以实现功能。
// ## 1. 框架上层逻辑,后续流程框架调用业务 ##
TransmittableThreadLocal context = new TransmittableThreadLocal();
context.set("value-set-in-parent");
// ## 2. 应用逻辑,后续流程业务调用框架下层逻辑 ##
ExecutorService executorService = Executors.newFixedThreadPool(3);
Runnable task = new Task("1");
Callable call = new Call("2");
executorService.submit(task);
executorService.submit(call);
// ## 3. 框架下层逻辑 ##
// Task或是Call中可以读取,值是"value-set-in-parent"
String value = context.get();
Maven依赖
com.alibaba
transmittable-thread-local
2.10.2
Java的启动参数配置
在Java的启动参数加上:-javaagent:path/to/transmittable-thread-local-2.x.x.jar。
如果修改了下载的TTL的Jar的文件名(transmittable-thread-local-2.x.x.jar),则需要自己手动通过-Xbootclasspath JVM参数来显式配置:
比如修改文件名成ttl-foo-name-changed.jar,则还加上Java的启动参数:
-Xbootclasspath/a:path/to/ttl-foo-name-changed.jar
Java命令行示例如下:
java -javaagent:path/to/transmittable-thread-local-2.x.x.jar \
-cp classes \
com.alibaba.ttl.threadpool.agent.demo.AgentDemo
或是
java -javaagent:path/to/ttl-foo-name-changed.jar \
-Xbootclasspath/a:path/to/ttl-foo-name-changed.jar \
-cp classes \
com.alibaba.ttl.threadpool.agent.demo.AgentDemo
将封装好的TransmittableThreadLocal Jar包放在类目录下的某个文件夹下,例如agent,那么只需在启动参数加入:-javaagent:agent/transmittable-thread-local-xxx.jar即可完成修饰代码的植入。
4.总结
1、引入TransmittableThreadLocalj.jar ,通过TtlExecutors包装现有线程池,使用TransmittableThreadLocal代替InheritableThreadLocal传值,解决线程池复用导致的threadLocal值丢失问题,有一定的工作量。
2、通过java Agent 无侵入解决此问题,工作量小,效率高,需要运维的支持,而且对探针技术未实际使用过,存在一定风险。
ps:对于java Agent 还没研究过,后续研究透彻补充进去