README.md
<p align="center"> <a href="https://github.com/alibaba/transmittable-thread-local/actions/workflows/ci.yaml"></a> <a href="https://github.com/alibaba/transmittable-thread-local/actions/workflows/strong_ci.yaml"></a> <a href="https://app.codecov.io/gh/alibaba/transmittable-thread-local/tree/master"></a> <a href="https://openjdk.java.net/"></a> <a href="https://www.apache.org/licenses/LICENSE-2.0.html"></a> <a href="https://alibaba.github.io/transmittable-thread-local/apidocs/"></a> <a href="https://repo1.maven.org/maven2/com/alibaba/transmittable-thread-local/maven-metadata.xml"></a> <a href="https://github.com/alibaba/transmittable-thread-local/releases"></a> <a href="https://github.com/alibaba/transmittable-thread-local/stargazers"></a> <a href="https://github.com/alibaba/transmittable-thread-local/fork"></a> <a href="https://github.com/alibaba/transmittable-thread-local/network/dependents"></a> <a href="https://github.com/alibaba/transmittable-thread-local/issues"></a> <a href="https://github.com/alibaba/transmittable-thread-local/graphs/contributors"></a> <a href="https://github.com/alibaba/transmittable-thread-local"></a> <a href="https://gitpod.io/#https://github.com/alibaba/transmittable-thread-local"></a> </p>[!IMPORTANT] 🚧 这个分支是
TransmittableThreadLocal(TTL) v3,在开发中还没有发布。
v3的版本说明、工作项列表及其进展,参见 issue 432。👉 目前使用中的稳定发布版本
v2.x在 分支2.x上。
📖 English Documentation | 📖 中文文档
TTL的好处与必要性👉 TransmittableThreadLocal(TTL):在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题。一个Java标准库本应为框架/中间件设施开发提供的标配能力,本库功能聚焦 & 0依赖,支持Java 6~21。
JDK的InheritableThreadLocal类可以完成父线程到子线程的值传递。但对于使用线程池等会池化复用线程的执行组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把 任务提交给线程池时的ThreadLocal值传递到 任务执行时。
本库提供的TransmittableThreadLocal类继承并加强InheritableThreadLocal类,解决上述的问题,使用详见 User Guide。
整个TransmittableThreadLocal库的核心功能(用户API、线程池ExecutorService/ForkJoinPool/TimerTask及其线程工厂的Wrapper;开发者API、框架/中间件的集成API),只有 ~1000 SLOC代码行,非常精小。
欢迎 👏
[!NOTE] 从
TTL v2.13+开始,升级到Java 8。🚀
如果需要Java 6的支持,使用版本2.12.x<a href="https://repo1.maven.org/maven2/com/alibaba/transmittable-thread-local/maven-metadata.xml"></a>
ThreadLocal的需求场景即TransmittableThreadLocal的潜在需求场景,如果你的业务需要『在使用线程池等会池化复用线程的执行组件情况下传递ThreadLocal值』则是TransmittableThreadLocal目标场景。
下面是几个典型场景例子。
Request级CacheSDK传递信息各个场景的展开说明参见子文档 需求场景。
使用类TransmittableThreadLocal来保存值,并跨线程池传递。
TransmittableThreadLocal继承InheritableThreadLocal,使用方式也类似。相比InheritableThreadLocal,添加了protected的transmitteeValue()方法,用于定制 任务提交给线程池时 的ThreadLocal值传递到 任务执行时 的传递方式,缺省是简单的赋值传递。
注意:如果传递的对象(引用类型)会被修改,且没有做深拷贝(如直接传递引用或是浅拷贝),那么
JDK的InheritableThreadLocal.childValue()一样,需要使用者/业务逻辑注意保证传递对象的线程安全。具体使用方式见下面的说明。
父线程给子线程传递值。
示例代码:
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
// =====================================================
// 在父线程中设置
context.set("value-set-in-parent");
// =====================================================
// 在子线程中可以读取,值是"value-set-in-parent"
String value = context.get();
# 完整可运行的Demo代码参见SimpleDemo.kt。
这其实是InheritableThreadLocal的功能,应该使用InheritableThreadLocal来完成。
但对于使用线程池等会池化复用线程的执行组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把 任务提交给线程池时的ThreadLocal值传递到 任务执行时。
解决方法参见下面的这几种用法。
Runnable和Callable使用TtlRunnable和TtlCallable来修饰传入线程池的Runnable和Callable。
示例代码:
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));
上面演示了Runnable,Callable的处理类似
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。
省去每次Runnable和Callable传入线程池时的修饰,这个逻辑可以在线程池中完成。
通过工具类TtlExecutors完成,有下面的方法:
getTtlExecutor:修饰接口ExecutorgetTtlExecutorService:修饰接口ExecutorServicegetTtlScheduledExecutorService:修饰接口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。
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执行器组件(即如线程池)如下:
java.util.concurrent.ThreadPoolExecutor 和 java.util.concurrent.ScheduledThreadPoolExecutor
JdkExecutorTtlTransformlet.java。java.util.concurrent.ForkJoinTask(对应的执行器组件是java.util.concurrent.ForkJoinPool)
ForkJoinTtlTransformlet.java。从版本 2.5.1 开始支持。Java 8引入的CompletableFuture与(并行执行的)Stream底层是通过ForkJoinPool来执行,所以支持ForkJoinPool后,TTL也就透明支持了CompletableFuture与Stream。🎉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:falseTTL Agent参数的配置说明详见TtlAgent.java的JavaDoc。<code>ScheduledThreadPoolExecutor</code>实现更强壮,并且功能更丰富。 如支持配置线程池的大小(<code>Timer</code>只有一个线程);<code>Timer</code>在<code>Runnable</code>中抛出异常会中止定时执行。更多说明参见 <a href="https://alibaba.github.io/Alibaba-Java-Coding-Guidelines/#concurrency" rel="nofollow">10. <strong>Mandatory</strong> 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</a>。</p>
</details> </blockquote>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
<strong><em>注意</em></strong>:不能修改从<code>Maven</code>库下载的<code>TTL Jar</code>文件名(形如<code>transmittable-thread-local-2.x.y.jar</code>)。 如果修改了,则需要自己手动通过<code>-Xbootclasspath JVM</code>参数来显式配置(就像<code>TTL</code>之前的版本的做法一样)。</p>
<p>自动设置<code>TTL Jar</code>到<code>boot class path</code>的实现是通过指定<code>TTL Java Agent Jar</code>文件里<code>manifest</code>文件(<code>META-INF/MANIFEST.MF</code>)的<code>Boot-Class-Path</code>属性:</p> <p><code>Boot-Class-Path</code></p> <p>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.</p> <p>更多详见</p> <ul> <li><a href="https://docs.oracle.com/en/java/javase/21/docs/api/java.instrument/java/lang/instrument/package-summary.html#package.description" rel="nofollow"><code>Java Agent</code>规范 - <code>JavaDoc</code></a></li> <li><a href="https://docs.oracle.com/en/java/javase/21/docs/specs/jar/jar.html#jar-manifest" rel="nofollow">JAR File Specification - JAR Manifest</a></li> <li><a href="https://docs.oracle.com/javase/tutorial/deployment/jar/manifestindex.html" rel="nofollow">Working with Manifest Files - The Java™ Tutorials</a></li> </ul> </details> </blockquote>当前版本的Java API文档地址: https://alibaba.github.io/transmittable-thread-local/apidocs/
示例:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.14.4</version>
</dependency>
可以在 maven.org 查看可用的版本。
编译构建的环境要求: JDK 8+;用Maven常规的方式执行编译构建即可:
# 在工程中已经包含了符合版本要求的Maven,直接运行 工程根目录下的mvnw;并不需要先手动自己安装好Maven。
# 运行测试Case
./mvnw test
# 编译打包
./mvnw package
# 运行测试Case、编译打包、安装TTL库到Maven本地
./mvnw install
#####################################################
# 如果使用你自己安装的 maven,版本要求:maven 3.3.9+
mvn install
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: https://bugs.java.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的好处与必要性[!NOTE] 不读这一节,并不会影响你使用
TTL来解决你碰到的问题,可以放心跳过;读了 User Guide 就可以快速用起来了~ 😄 这一节信息密度较高不易读。
好处:透明且自动完成所有异步执行上下文的可定制、规范化的捕捉与传递。
这个好处也是TransmittableThreadLocal的目标。
必要性:随着应用的分布式微服务化并使用各种中间件,越来越多的功能与组件会涉及不同的上下文,逻辑流程也越来越长;上下文问题实际上是个大的易错的架构问题,需要统一的对业务透明的解决方案。
使用ThreadLocal作为业务上下文传递的经典技术手段在中间件、技术与业务框架中广泛大量使用。而对于生产应用,几乎一定会使用线程池等异步执行组件,以高效支撑线上大流量。但使用ThreadLocal及其set/remove的上下文传递模式,在使用线程池等异步执行组件时,存在多方面的问题:
1. 从业务使用者角度来看
ThreadLocal上下文各自的获取的逻辑或类。RPC的上下文(如Dubbo的RpcContext)、全链路跟踪的上下文(如SkyWalking的ContextManager)、不同业务模块中的业务流程上下文,等等。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,欢迎继续讨论 ♥️
使用了TTL的一部分开源项目:
sofastack/sofa-rpc trpc-group/trpc-java tencentmusic/supersonic dromara/hmily dromara/gobrs-async dromara/dynamic-tp opengoofy/hippo4j siaorg/sia-gateway huaweicloud/Sermant ZTO-Express/zms lxchinesszz/tomato ytyht226/taskflow foldright/cffu tuya/connector apache/shardingsphere apache/kylin mybatis-flex/mybatis-flex basicai/xtreme1 oceanbase/odc sagframe/sagacity-sqltoy dromara/stream-query luo-zhan/Transformer SimonAlong/Neo ppdaicorp/das didi/ALITA didi/daedalus dromara/liteflow alibaba/bulbasaur dromara/TLog fayechenlong/plumelog minbox-projects/minbox-logging minbox-projects/api-boot ofpay/logback-mdc-ttl oldratlee/log4j2-ttl-thread-context-map qqxx6661/log-record
业务日志记录框架,使用注解优雅记录日志,支持SpEL表达式,自定义上下文,自定义函数,实体类DIFF等特性。ymm-tech/easy-byte-coder OpenBankProject/OBP-API gz-yami/mall4j Joolun/JooLun-wx HummerRisk/HummerRisk XiaoMi/mone Mone以微服务为核心的一站式企业协同研发平台。支持公共云、专有云和混合云多种部署形态;提供从“项目创建->开发->部署->治理->应用观测”端到端的研发全流程服务;通过云原生新技术和研发新模式,打造“双敏”,敏捷研发和敏捷组织,保障小米-中国区高复杂业务、大规模团队的敏捷研发协同,实现多倍效能提升。yangzongzhuan/RuoYi-Cloud somowhere/albedo qwdigital/LinkWechat fushengqian/fuint hiparker/opsli-boot topiam/eiam Newspiral/newspiral-business ssssssss-team/spider-flow nekolr/slime Jackson0714/PassJava-Platform martin-chips/DimpleBlog SpringBoot2搭建的个人博客系统zjcscut/octopus xggz/mqr alibaba/jvm-sandbox-repeater vivo/MoonBox alibaba/testable-mock shulieTech/Takin shulieTech/LinkAgent alibaba/virtual-environment Kubernetes版实现Spring Cloud/Spring Boot的框架方案/脚手架
YunaiV/ruoyi-vue-pro YunaiV/yudao-cloud zlt2000/microservices-platform dromara/lamp-cloud zuihou/lamp-util matevip/matecloud gavenwangcn/vole liuweijw/fw-cloud-framework liuht777/Taroco mingyang66/spring-parent budwk/budwk BudWk 原名 NutzWk ,基于国产框架 nutz 及 nutzboot 开发的开源Web基础项目,集权限体系、系统参数、数据字典、站内消息、定时任务、CMS、微信等最常用功能,不庞杂、不面面俱到,使其具有上手容易、开发便捷、扩展灵活等特性,特别适合各类大中小型定制化项目需求yinjihuan/spring-cloud louyanfeng25/ddd-demo nageoffer/12306