ThreadLocal的坑--ThreadLocal跨线程传递问题

1、父子线程间的传递问题

 ThreadLocal的子类InheritableThreadLocal其实已经帮我们处理好了,通过这个组件可以实现父子线程之间的数据传递,在子线程中能够父线程中的ThreadLocal本地变量。

我们发现InheritableThreadLocal中createMap,以及getMap方法处理的对象不一样了,其中在ThreadLocal中处理的是threadLocals,而InheritableThreadLocal中的是inheritableThreadLocals。

代码的意思是在Thread获取先父亲线程parent(即要创建子线程的当前这个线程)。当父亲线程中对inherThreadLocals进行了赋值,就会把当前线程的本地变量(也就是父线程的inherThreadLocals)进行createInheritedMap方法操作。查看源码createInheritedMap方法,源码可知此操作就是将赋线程的threadLocalMap传递给子线程。

参考:https://www.cnblogs.com/Nonnetta/p/10175662.html

2、使用异步线程池时的传递问题

这里有两种解决方案

1、参考Hystrix中的回调方法

在SpringCloud中的分布式链路跟踪时,traceId如何在异步线程中传递traceId呢?

看源码可以知道是通过:
Sluth是通过实现HystrixConcurrencyStrategy接口来解决traceId异步传递的问题。Hystrix在实际调用时,会调用HystrixConcurrencyStrategy的wrapCallable方法。因此,通过实现这个接口,在wrapCallable中将traceId存放起来(具体参见SleuthHystrixConcurrencyStrategy)

https://blog.csdn.net/yaowwwww7071/article/details/85769505

2、线程池采用阿里的transmittable-thread-local

通过inheritableThreadLocals我们可以在父线程创建子线程的时候将Local中的值传递给子线程,这个特性已经能够满足大部分的需求了,但是还有一个很严重的问题是如果是在线程复用的情况下就会出问题,比如线程池中去使用inheritableThreadLocals 进行传值,因为inheritableThreadLocals 只是会再新创建线程的时候进行传值,线程复用并不会做这个操作,那么要解决这个问题就得自己去扩展线程类,实现这个功能。阿里开源的transmittable-thread-local。GitHub地址:https://github.com/alibaba/transmittable-thread-local

TTL(transmittable-thread-local)是一个线程间传递ThreadLocal,异步执行时上下文传递的解决方案。整个库的核心是构建在TransmittableThreadLocal类(继承并加强InheritableThreadLocal类)之上,同时包含线程池修饰(ExecutorService/ForkJoinPool/TimerTask)以及Java Agent支持,代码小于1k行,短小精悍。

下面是几个典型场景例子。

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

各个场景的展开说明参见子文档 需求场景

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

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

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

具体使用方式见下面的说明。

2. 保证线程池中传递值(3种方式:修饰Runnable和Callable,修饰线程池,Java Agent来修饰JDK线程池实现类)

2.1 修饰RunnableCallable

使用TtlRunnableTtlCallable来修饰传入线程池的RunnableCallable

示例代码:

TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();

// =====================================================

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

Runnable task = new RunnableTask();
// 额外的处理,生成修饰了的对象ttlRunnable
Runnable ttlRunnable = TtlRunnable.get(task);
executorService.submit(ttlRunnable);

// =====================================================

// Task中可以读取,值是"value-set-in-parent"
String value = context.get();

注意
即使是同一个Runnable任务多次提交到线程池时,每次提交时都需要通过修饰操作(即TtlRunnable.get(task))以抓取这次提交时的TransmittableThreadLocal上下文的值;即如果同一个任务下一次提交时不执行修饰而仍然使用上一次的TtlRunnable,则提交的任务运行时会是之前修饰操作所抓取的上下文。示例代码如下:

// 第一次提交
Runnable task = new RunnableTask();
executorService.submit(TtlRunnable.get(task));

// ...业务逻辑代码,
// 并且修改了 TransmittableThreadLocal上下文 ...
// context.set("value-modified-in-parent");

// 再次提交
// 重新执行修饰,以传递修改了的 TransmittableThreadLocal上下文
executorService.submit(TtlRunnable.get(task));

上面演示了RunnableCallable的处理类似

TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();

// =====================================================

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

Callable call = new CallableTask();
// 额外的处理,生成修饰了的对象ttlCallable
Callable ttlCallable = TtlCallable.get(call);
executorService.submit(ttlCallable);

// =====================================================

// Call中可以读取,值是"value-set-in-parent"
String value = context.get();

# 完整可运行的Demo代码参见TtlWrapperDemo.kt

 

整个过程的完整时序图

时序图

 

2.2 修饰线程池

省去每次RunnableCallable传入线程池时的修饰,这个逻辑可以在线程池中完成。

通过工具类com.alibaba.ttl.threadpool.TtlExecutors完成,有下面的方法:

  • getTtlExecutor:修饰接口Executor
  • getTtlExecutorService:修饰接口ExecutorService
  • getTtlScheduledExecutorService:修饰接口ScheduledExecutorService

示例代码:

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

TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();

// =====================================================

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

Runnable task = new RunnableTask();
Callable call = new CallableTask();
executorService.submit(task);
executorService.submit(call);

// =====================================================

// Task或是Call中可以读取,值是"value-set-in-parent"
String value = context.get();

# 完整可运行的Demo代码参见TtlExecutorWrapperDemo.kt

 

2.3 使用Java Agent来修饰JDK线程池实现类

这种方式,实现线程池的传递是透明的,业务代码中没有修饰Runnable或是线程池的代码。即可以做到应用代码 无侵入
# 关于 无侵入 的更多说明参见文档Java Agent方式对应用代码无侵入

示例代码:

// ## 1. 框架上层逻辑,后续流程框架调用业务 ##
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
context.set("value-set-in-parent");

// ## 2. 应用逻辑,后续流程业务调用框架下层逻辑 ##
ExecutorService executorService = Executors.newFixedThreadPool(3);

Runnable task = new RunnableTask();
Callable call = new CallableTask();
executorService.submit(task);
executorService.submit(call);

// ## 3. 框架下层逻辑 ##
// Task或是Call中可以读取,值是"value-set-in-parent"
String value = context.get();

Demo参见AgentDemo.kt。执行工程下的脚本scripts/run-agent-demo.sh即可运行Demo。

目前TTL Agent中,修饰了的JDK执行器组件(即如线程池)如下:

  1. java.util.concurrent.ThreadPoolExecutorjava.util.concurrent.ScheduledThreadPoolExecutor
  2. java.util.concurrent.ForkJoinTask(对应的执行器组件是java.util.concurrent.ForkJoinPool
    • 修饰实现代码在ForkJoinTtlTransformlet.java。从版本 2.5.1 开始支持。
    • 注意Java 8引入的CompletableFuture与(并行执行的)Stream底层是通过ForkJoinPool来执行,所以支持ForkJoinPool后,TTL也就透明支持了CompletableFutureStream。🎉
  3. java.util.TimerTask的子类(对应的执行器组件是java.util.Timer
    • 修饰实现代码在TimerTaskTtlTransformlet.java。从版本 2.7.0 开始支持。
    • 注意:从2.11.2版本开始缺省开启TimerTask的修饰(因为保证正确性是第一位,而不是最佳实践『不推荐使用TimerTask』:);2.11.1版本及其之前的版本没有缺省开启TimerTask的修饰。
    • 使用Agent参数ttl.agent.enable.timer.task开启/关闭TimerTask的修饰:
      • -javaagent:path/to/transmittable-thread-local-2.x.y.jar=ttl.agent.enable.timer.task:true
      • -javaagent:path/to/transmittable-thread-local-2.x.y.jar=ttl.agent.enable.timer.task:false
    • 更多关于TTL Agent参数的配置说明详见TtlAgent.java的JavaDoc

关于java.util.TimerTask/java.util.Timer

TimerJDK 1.3的老类,不推荐使用Timer类。

推荐用ScheduledExecutorService
ScheduledThreadPoolExecutor实现更强壮,并且功能更丰富。 如支持配置线程池的大小(Timer只有一个线程);TimerRunnable中抛出异常会中止定时执行。更多说明参见10. Mandatory Run multiple TimeTask by using ScheduledExecutorService rather than Timer because Timer will kill all running threads in case of failing to catch exceptions. - Alibaba Java Coding Guidelines

 

Java Agent的启动参数配置

Java的启动参数加上:-javaagent:path/to/transmittable-thread-local-2.x.y.jar

注意

  • 如果修改了下载的TTLJar的文件名(transmittable-thread-local-2.x.y.jar),则需要自己手动通过-Xbootclasspath JVM参数来显式配置。
    比如修改文件名成ttl-foo-name-changed.jar,则还需要加上Java的启动参数:-Xbootclasspath/a:path/to/ttl-foo-name-changed.jar
  • 或使用v2.6.0之前的版本(如v2.5.1),则也需要自己手动通过-Xbootclasspath JVM参数来显式配置(就像TTL之前的版本的做法一样)。
    加上Java的启动参数:-Xbootclasspath/a:path/to/transmittable-thread-local-2.5.1.jar

Java命令行示例如下:

java -javaagent:path/to/transmittable-thread-local-2.x.y.jar \
    -cp classes \
    com.alibaba.demo.ttl.agent.AgentDemo

# 如果修改了TTL jar文件名 或 TTL版本是 2.6.0 之前
# 则还需要显式设置 -Xbootclasspath 参数
java -javaagent:path/to/ttl-foo-name-changed.jar \
    -Xbootclasspath/a:path/to/ttl-foo-name-changed.jar \
    -cp classes \
    com.alibaba.demo.ttl.agent.AgentDemo

java -javaagent:path/to/transmittable-thread-local-2.5.1.jar \
    -Xbootclasspath/a:path/to/transmittable-thread-local-2.5.1.jar \
    -cp classes \
    com.alibaba.demo.ttl.agent.AgentDemo

 

关于boot class path

因为修饰了JDK标准库的类,标准库由bootstrap class loader加载;修饰后的JDK类引用了TTL的代码,所以Java Agent使用方式下TTL Jar文件需要配置到boot class path上。

TTLv2.6.0开始,加载TTL Agent时会自动设置TTL Jarboot class path上。
注意:不能修改从Maven库下载的TTL Jar文件名(形如transmittable-thread-local-2.x.y.jar)。 如果修改了,则需要自己手动通过-Xbootclasspath JVM参数来显式配置(就像TTL之前的版本的做法一样)。

自动设置TTL Jarboot class path的实现是通过指定TTL Java Agent Jar文件里manifest文件(META-INF/MANIFEST.MF)的Boot-Class-Path属性:

Boot-Class-Path

A list of paths to be searched by the bootstrap class loader. Paths represent directories or libraries (commonly referred to as JAR or zip libraries on many platforms). These paths are searched by the bootstrap class loader after the platform specific mechanisms of locating a class have failed. Paths are searched in the order listed.

转:https://github.com/alibaba/transmittable-thread-local

posted on 2016-04-28 23:38  duanxz  阅读(4996)  评论(1编辑  收藏  举报