Need help with transmittable-thread-local?
Click the “chat” button below for chat support from the developer who created it, or find similar developers for support.

About the developer

alibaba
3.8K Stars 1.1K Forks Apache License 2.0 774 Commits 26 Opened issues

Description

📌 TransmittableThreadLocal(TTL), the missing Java™ std lib(simple & 0-dependency) for framework/middleware, provide an enhanced InheritableThreadLocal that transmits values between threads even using thread pooling components.

Services available

!
?

Need anything else?

Contributors list

📌 TransmittableThreadLocal(TTL) 📌

Build Status Windows Build Status Coverage Status Maintainability JDK support
License Javadocs Maven Central GitHub release
Chat at gitter.im GitHub Stars GitHub Forks GitHub repo dependents GitHub issues GitHub Contributors

📖 English Documentation | 📖 中文文档



🔧 功能

👉 在使用线程池等会池化复用线程的执行组件情况下,提供

ThreadLocal
值的传递功能,解决异步执行时上下文传递的问题。 一个
Java
标准库本应为框架/中间件设施开发提供的标配能力,本库功能聚焦 & 0依赖,支持
Java
17/16/15/14/13/12/11/10/9/8/7/6。

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

本库提供的

TransmittableThreadLocal
类继承并加强

InheritableThreadLocal
类,解决上述的问题,使用详见 User Guide

整个

TransmittableThreadLocal
库的核心功能(用户
API
与框架/中间件的集成
API
、线程池
ExecutorService
/
ForkJoinPool
/
TimerTask
及其线程工厂的
Wrapper
),只有 ~1000
SLOC
代码行
,非常精小。

欢迎 👏

🎨 需求场景

ThreadLocal
的需求场景即
TransmittableThreadLocal
的潜在需求场景,如果你的业务需要『在使用线程池等会池化复用线程的执行组件情况下传递
ThreadLocal
值』则是
TransmittableThreadLocal
目标场景。

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

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

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

👥 User Guide

使用类

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

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

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

1. 简单使用

父线程给子线程传递值。

示例代码:

TransmittableThreadLocal context = new TransmittableThreadLocal<>();

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

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

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

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

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

SimpleDemo.kt

这是其实是

InheritableThreadLocal
的功能,应该使用
InheritableThreadLocal
来完成。

但对于使用线程池等会池化复用线程的执行组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的

ThreadLocal
值传递已经没有意义,应用需要的实际上是把 任务提交给线程池时
ThreadLocal
值传递到 任务执行时

解决方法参见下面的这几种用法。

2. 保证线程池中传递值

2.1 修饰
Runnable
Callable

使用

TtlRunnable
TtlCallable
来修饰传入线程池的

Runnable
Callable

示例代码:

TransmittableThreadLocal 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
Callable
的处理类似
TransmittableThreadLocal 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 修饰线程池

省去每次

Runnable
Callable
传入线程池时的修饰,这个逻辑可以在线程池中完成。

通过工具类

com.alibaba.ttl.threadpool.TtlExecutors
完成,有下面的方法:

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

示例代码:

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

TransmittableThreadLocal 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 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.ThreadPoolExecutor
    java.util.concurrent.ScheduledThreadPoolExecutor
  2. java.util.concurrent.ForkJoinTask
    (对应的执行器组件是
    java.util.concurrent.ForkJoinPool
    • 修饰实现代码在
      ForkJoinTtlTransformlet.java
      。从版本
      2.5.1
      开始支持。
    • 注意
      Java 8
      引入的
      CompletableFuture
      与(并行执行的)
      Stream
      底层是通过
      ForkJoinPool
      来执行,所以支持
      ForkJoinPool
      后,
      TTL
      也就透明支持了
      CompletableFuture
      Stream
      。🎉
  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

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

推荐用

ScheduledExecutorService

ScheduledThreadPoolExecutor
实现更强壮,并且功能更丰富。 如支持配置线程池的大小(
Timer
只有一个线程);
Timer
Runnable
中抛出异常会中止定时执行。更多说明参见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

注意

  • 如果修改了下载的
    TTL
    Jar
    的文件名(
    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
上。

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

自动设置

TTL Jar
boot 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.

更多详见

🔌 Java API Docs

当前版本的Java API文档地址: https://alibaba.github.io/transmittable-thread-local/apidocs/

🍪 Maven依赖

示例:

    com.alibaba
    transmittable-thread-local
    2.12.1

可以在 search.maven.org 查看可用的版本。

🔨 关于编译构建与
IDE
开发

编译构建的环境要求:

JDK 8~11
;用

Maven
常规的方式执行编译构建即可:
# 在工程中已经包含了符合版本要求的
Maven
,直接运行 工程根目录下的
mvnw
;并不需要先手动自己安装好
Maven
# 运行测试Case
./mvnw test
# 编译打包
./mvnw package
# 运行测试Case、编译打包、安装TTL库到Maven本地
./mvnw install

#####################################################

如果使用你自己安装的 maven,版本要求:maven 3.3.9+

mvn install

如何用

IDE
来开发时注意点,更多说明参见 文档 如何用
IDE
开发 - Developer Guide

❓ FAQ

Q1.

TTL Agent
与其它
Agent
(如
Skywalking
Promethues
)配合使用时不生效?

配置

TTL Agent
在最前的位置,可以避免与其它其它
Agent
配合使用时,
TTL Agent
可能的不生效问题。配置示例:
java -javaagent:path/to/transmittable-thread-local-2.x.y.jar \
     -javaagent:path/to/skywalking-agent.jar \
     -jar your-app.jar

原因是:

  • Skywalking
    这样的
    Agent
    的入口逻辑(
    premain
    )包含了线程池的启动。
  • 如果配置在这样的
    Agent
    配置在前面,到了
    TTL Agent
    (的
    premain
    )时,
    TTL
    需要加强的线程池类已经加载(
    load
    )了。
  • TTL Agent
    TtlTransformer
    是在类加载时触发类的增强;如果类已经加载了会跳过
    TTL Agent
    的增强逻辑。

更多讨论参见 Issue:

TTL agent
与其他
Agent
的兼容性问题 #226

Q2.

MacOS
下,使用
Java Agent
,可能会报
JavaLaunchHelper
的出错信息

JDK Bug: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=8021205
可以换一个版本的

JDK
。我的开发机上
1.7.0_40
有这个问题,
1.6.0_51
1.7.0_45
可以运行。
#
1.7.0_45
还是有
JavaLaunchHelper
的出错信息,但不影响运行。

✨ 使用
TTL
的好处与必要性

注:不读这一节,并不会影响你使用

TTL
来解决你碰到的问题,可以放心跳过;读了 User Guide 就可以快速用起来了~ 😄 这一节信息密度较高不易读。

好处:透明且自动完成所有异步执行上下文的可定制、规范化的捕捉与传递。
这个好处也是

TransmittableThreadLocal
的目标。

必要性:随着应用的分布式微服务化并使用各种中间件,越来越多的功能与组件会涉及不同的上下文,逻辑流程也越来越长;上下文问题实际上是个大的易错的架构问题,需要统一的对业务透明的解决方案。

使用

ThreadLocal
作为业务上下文传递的经典技术手段在中间件、技术与业务框架中广泛大量使用。而对于生产应用,几乎一定会使用线程池等异步执行组件,以高效支撑线上大流量。但使用
ThreadLocal
及其
set/remove
的上下文传递模式,在使用线程池等异步执行组件时,存在多方面的问题:

1. 从业务使用者角度来看

  1. 繁琐
    • 业务逻辑要知道:有哪些上下文;各个上下文是如何获取的。
    • 并需要业务逻辑去一个一个地捕捉与传递。
  2. 依赖
    • 需要直接依赖不同
      ThreadLocal
      上下文各自的获取的逻辑或类。
    • RPC
      的上下文(如
      Dubbo
      RpcContext
      )、全链路跟踪的上下文(如
      SkyWalking
      ContextManager
      )、不同业务模块中的业务流程上下文,等等。
  3. 静态(易漏)
    • 因为要 事先 知道有哪些上下文,如果系统出现了一个新的上下文,业务逻辑就要修改添加上新上下文传递的几行代码。也就是说因 系统的 上下文新增,业务的 逻辑就跟进要修改。
    • 而对于业务来说,不关心系统的上下文,即往往就可能遗漏,会是线上故障了。
    • 随着应用的分布式微服务化并使用各种中间件,越来越多的功能与组件会涉及不同的上下文,逻辑流程也越来越长;上下文问题实际上是个大的易错的架构问题,需要统一的对业务透明的解决方案。
  4. 定制性
    • 因为需要业务逻辑来完成捕捉与传递,业务要关注『上下文的传递方式』:直接传引用?还是拷贝传值?拷贝是深拷贝还是浅拷贝?在不同的上下文会需要不同的做法。
    • 『上下文的传递方式』往往是 上下文的提供者(或说是业务逻辑的框架部分)才能决策处理好的;而 上下文的使用者(或说是业务逻辑的应用部分)往往不(期望)知道上下文的传递方式。这也可以理解成是 依赖,即业务逻辑 依赖/关注/实现了 系统/架构的『上下文的传递方式』。

2. 从整体流程实现角度来看

关注的是 上下文传递流程的规范化。上下文传递到了子线程要做好 清理(或更准确地说是要 恢复 成之前的上下文),需要业务逻辑去处理好。如果业务逻辑对清理的处理不正确,比如:

  • 如果清理操作漏了:
    • 下一次执行可能是上次的,即『上下文的 污染/串号』,会导致业务逻辑错误。
    • 『上下文的 泄漏』,会导致内存泄漏问题。
  • 如果清理操作做多了,会出现上下文 丢失

上面的问题,在业务开发中引发的

Bug
真是屡见不鲜 !本质原因是:
ThreadLocal
set/remove
的上下文传递模式
在使用线程池等异步执行组件的情况下不再是有效的。常见的典型例子:
  • 当线程池满了且线程池的
    RejectedExecutionHandler
    使用的是
    CallerRunsPolicy
    时,提交到线程池的任务会在提交线程中直接执行,
    ThreadLocal.remove
    操作清理提交线程的上下文导致上下文丢失
  • 类似的,使用
    ForkJoinPool
    (包含并行执行
    Stream
    CompletableFuture
    ,底层使用
    ForkJoinPool
    )的场景,展开的
    ForkJoinTask
    会在任务提交线程中直接执行。同样导致上下文丢失

怎么设计一个『上下文传递流程』方案(即上下文的生命周期),以保证没有上面的问题?

期望:上下文生命周期的操作从业务逻辑中分离出来。业务逻辑不涉及生命周期,就不会有业务代码如疏忽清理而引发的问题了。整个上下文的传递流程或说生命周期可以规范化成:捕捉、回放和恢复这3个操作,即

CRR(capture/replay/restore)
模式。更多讨论参见 Issue:能在详细讲解一下
replay
restore
的设计理念吗?#201

总结上面的说明:在生产应用(几乎一定会使用线程池等异步执行组件)中,使用

ThreadLocal
及其
set/remove
的上下文传递模式几乎一定是有问题的只是在等一个出
Bug
的机会

更多

TTL
好处与必要性的展开讨论参见 Issue:这个库带来怎样的好处和优势? #128,欢迎继续讨论 ♥️

🗿 更多文档

📚 相关资料

JDK Core Classes

👷 Contributors

  • Jerry Lee <oldratlee at gmail dot com> @oldratlee
  • Yang Fang <snoop.fy at gmail dot com> @driventokill
  • Zava Xu <zava.kid at gmail dot com> @zavakid
  • wuwen <wuwen.55 at aliyun dot com> @wuwen5
  • Xiaowei Shi <179969622 at qq dot com> @xwshiustc
  • David Dai <351450944 at qq dot com> @LNAmp
  • Your name here :-)

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.