【通用行业开发部】阿里开源TransmittableThreadLocal使用经验记录

简介: 本文章主要记录我在一次业务优化中,使用线程池带来的子父线程值传递问题,及使用TransmittableThreadLocal来解决该问题的经验,并对TransmittableThreadLocal原理做些梳理。

业务背景

  前段时间在工作项目中,被指派一项优化任务,由于原功能耗时较久,影响使用体感;梳理完该功能涉及到的业务后,发现具备复杂、量大等特点。

    将一个大体量数据循环分割成若干小循环,并行执行,结果汇总,是该优化方案中的一环。但原业务中使用ThreadLocal。

    ThreadLocal是解决线程间数据隔离性的,源于它将值存储在每个线程Thread的自己变量(ThreadLocal.ThreadLocalMap)中,但是ThreadLocal有个弊端,由于线程间隔离性,子线程无法捕获父线程ThreadLocalMap里的值。一旦使用多线程,需要解决值传递问题。


尝试使用InheritableThreadLocal(Itl)解决问题

   Thread线程有两个ThreadLocal.ThreadLocalMap 类型变量threadLocals 和 inheritableThreadLocals。

ThreadLocal.ThreadLocalMapthreadLocals=null;
ThreadLocal.ThreadLocalMapinheritableThreadLocals=null;

inheritableThreadLocals 是在Thread 初始化方法init中赋值。

if (inheritThreadLocals&&parent.inheritableThreadLocals!=null)
this.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); // 创建一个新ThreadLocalMap 并将父线程值传递过去

    InheritableThreadLocal是可以解决子父线程值传递问题。我使用Itl 代替了Tl。待优化工作完毕,本地调试及测试通过后。发布到测试环境验证中,在测试环境中发现功能出现了异常,重新回归到本地测试,本地功能多次反复点测却又是完好的。我一度认为环境问题导致,却毫无根据。开始输出日志定位排查,发现测试环境部分线程没有获取到主线程set的值。

    开始思考其中原因。想到了线程池问题,项目中使用了线程池,点测该功能前,线程池是有部分线程存于活跃状态,该部分线程没有经过创建时值传递过程,在执行该功能时,获取不到父线程set的值。

     本地环境却是好的,这也不奇怪了。在我本地程序重启后,直接点测了该功能,线程池中的线程都是在该任务中创建的,全部携带了父线程set的值。

使用TransmittableThreadLocal解决线程池引起的问题

    查阅了资源,决定使用Alibaba开源的TransmittableThreadLocal,根据官网描述:TransmittableThreadLocal(简称TTL),"在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能,解决异步执行时上下文传递的问题"。在使用TransmittableThreadLocal后,测试环境线程池引发的问题确实得到了解决。

官网地址:https://github.com/alibaba/transmittable-thread-local

知其然知其所以然

Ttl定义:

TransmittableThreadLocal<T>extendsInheritableThreadLocal<T>//集成了Itl,具备Itl的功能.子线程被创建后,继承了父线程的值

Ttl有个类变量hold:

/*** hold是一个InheritableThreadLocal类型变量,持有WeakHashMap,重写了initialValue()和childValue()方法,* 异步时父线程的属性是直接作为初始化值赋值给子线程的本地变量对象*/privatestaticfinalInheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>holder=newInheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>() {
@OverrideprotectedWeakHashMap<TransmittableThreadLocal<Object>, ?>initialValue() {
returnnewWeakHashMap<TransmittableThreadLocal<Object>, Object>();
                }
@OverrideprotectedWeakHashMap<TransmittableThreadLocal<Object>, ?>childValue(WeakHashMap<TransmittableThreadLocal<Object>, ?>parentValue) {
returnnewWeakHashMap<TransmittableThreadLocal<Object>, Object>(parentValue);
                }
            };

该hold变量数据来源:

publicfinalTget() {
Tvalue=super.get();
if (disableIgnoreNullValueSemantics||null!=value) addThisToHolder();  //往hold变量添加值returnvalue;
    }
@Overridepublicfinalvoidset(Tvalue) {
if (!disableIgnoreNullValueSemantics&&null==value) {
// 如果值为空 就移除掉remove();
        } else {
super.set(value);
addThisToHolder();//往hold变量添加值        }
    }
@SuppressWarnings("unchecked")
privatevoidaddThisToHolder() {
if (!holder.get().containsKey(this)) {
//开始往hold 变量中存值holder.get().put((TransmittableThreadLocal<Object>) this, null); // WeakHashMap 支撑null值//这里打印一句话 方面梳理整个工作流程System.out.println(String.format("%s线程已成功在hold变量中存储值。key:%s,value=%s",Thread.currentThread().getName(),this.hashCode(),this.get()));
        }
    }

以上代码做个总结Ttl有一个InheritableThreadLocal类型hold变量,通过线程的set,get操作触发addThisToHolder()方法进行添加值。

另外需要注意的是:Ttl不是直接执行Runnable的run方法,而是通过TtlRunnable对run方法做了一层封装。

先看下Runnable 构造方法:

privateTtlRunnable(@NonNullRunnablerunnable, booleanreleaseTtlValueReferenceAfterRun) {
this.capturedRef=newAtomicReference<Object>(capture()); //会触发 capture()方法this.runnable=runnable;
this.releaseTtlValueReferenceAfterRun=releaseTtlValueReferenceAfterRun;
    }

TtlRunnable构造方法中,会触发capture(),将数据(主线程set的值)储存在capturedRef变量中。

接着看下capture() 方法:

publicstaticObjectcapture() {
//从父线程捕获的值形成快照对象returnnewSnapshot(captureTtlValues(), captureThreadLocalValues());
        }
privatestaticHashMap<TransmittableThreadLocal<Object>, Object>captureTtlValues() {
HashMap<TransmittableThreadLocal<Object>, Object>ttl2Value=newHashMap<TransmittableThreadLocal<Object>, Object>();
//这里打印一下语句 方便掌握整个流程System.out.println(String.format("----------------------------------------------------------------------------"));
System.out.println(String.format("capture()开始从hold变量捕获数据"));
inti=0;
/*** 这里遍历hold,取出父线程set进去的值,存储到HashMap类型的ttl2Value变量中*/for (TransmittableThreadLocal<Object>threadLocal : holder.get().keySet()) {
ttl2Value.put(threadLocal, threadLocal.copyValue());
i++;
System.out.println(String.format("从hold变量中拿到的数据%s:key=%s, value=%S",i,threadLocal.hashCode(),threadLocal.get()));
            }
returnttl2Value;
        }

以上代码做个总结:TtlRunnable在构建时,触发capture()方法,将父线程存储到hold变量中的值形成快照(Snapshot)存档下来。

接着看TtlRunnable的run方法:

publicvoidrun() {
finalObjectcaptured=capturedRef.get();  //capture()方法返回的值if (captured==null||releaseTtlValueReferenceAfterRun&&!capturedRef.compareAndSet(captured, null)) {
thrownewIllegalStateException("TTL value reference is released after run!");
        }
/*** replay方法比较关键, 做了几件事* 1、遍历hold变量,然后将数据备份到map类型的backup变量中* 2、判断hold变量中是否存在captured变量中不存在的元素,如有,将从hold移除* 3、为当前子线程赋值* backup用于恢复数据,任务执行完毕,需要回归线程池,需要恢复其原本变量值*/finalObjectbackup=replay(captured);
try {
runnable.run(); //此时子线程已有值,可以执行业务        } finally {
/*** 1、根据backup备份数据,将hold数据复原* 2、根据backup备份数据,将子线程重新恢复原有数值*/restore(backup);
        }
    }

TtlRunnable的run方法中, replay()和restore()方法比较关键。

先看 replay()方法:

@NonNullpublicstaticObjectreplay(@NonNullObjectcaptured) {
finalSnapshotcapturedSnapshot= (Snapshot) captured;
returnnewSnapshot(replayTtlValues(capturedSnapshot.ttl2Value), replayThreadLocalValues(capturedSnapshot.threadLocal2Value));
        }
@NonNullprivatestaticHashMap<TransmittableThreadLocal<Object>, Object>replayTtlValues(@NonNullHashMap<TransmittableThreadLocal<Object>, Object>captured) {
HashMap<TransmittableThreadLocal<Object>, Object>backup=newHashMap<TransmittableThreadLocal<Object>, Object>();
//以下打印语句 方面梳理整个工作流程System.out.println("--------------------------------------------------------------");
System.out.println(String.format("replayTtlValues开始,%s线程从holder变量里的获取数据列表",Thread.currentThread().getName()));
//此循环是我手动写的 源码没有inti=0;
for (Iterator<TransmittableThreadLocal<Object>>iterator=holder.get().keySet().iterator(); iterator.hasNext();){
TransmittableThreadLocal<Object>threadLocal=iterator.next();
i++;
System.out.println(String.format("%s线程从hold拿到的数据%s:key=%s, value=%s",Thread.currentThread().getName(), i,threadLocal.hashCode(), threadLocal.get()));
            }
for (finalIterator<TransmittableThreadLocal<Object>>iterator=holder.get().keySet().iterator(); iterator.hasNext(); ) {
TransmittableThreadLocal<Object>threadLocal=iterator.next();
backup.put(threadLocal, threadLocal.get());
/***从hold变量清理点captured变量中不存在的值,避免在执行业务(run方法)时存在数据污染** 注意:captured存储的变量和此时hold存储的变量有可能不一致* 原因:captured 变量存储的是父线程放进去的值。在TtlRunnable构建方法中触发capture() 存入到captured的值  * 而此时从hold.get()获取的值,是子线程被创建时从父线获取的。触发了Thread init初始化方法。** 为什么要执行该清理操作?* 这里需要举例子说明: 如果子线程在被创建是已经从父现在那里获取到了值 value=3,该子线程在线程池中如果没有消灭,在某个主线程中再次* 执行任务时,假如该主线程 没有设置value=3这个值,此时captured变量中就不会有value=3 那么该子线程执行run方法时 就会取到vlue=3这个值,污染了业务数据**/if (!captured.containsKey(threadLocal)) {
iterator.remove();
threadLocal.superRemove();
                }
            }
/*** 这里将captured变量中的值 set到本线程中,进而触发了addThisToHolder()方法,* 该方法执行后,hold变量有可能会发生变化*/setTtlValuesTo(captured);
// 用于功能拓展doExecuteCallback(true);
//以下打印语句 方面梳理整个工作流程System.out.println("--------------------------------------------------------------");
System.out.println(String.format("replayTtlValues结束,%s线程从holder变量里的获取数据列表",Thread.currentThread().getName()));
i=0;
for (Iterator<TransmittableThreadLocal<Object>>iterator=holder.get().keySet().iterator(); iterator.hasNext();){
TransmittableThreadLocal<Object>threadLocal=iterator.next();
i++;
System.out.println(String.format("%s线程从hold拿到的数据%s:key=%s, value=%s",Thread.currentThread().getName(), i,threadLocal.hashCode(), threadLocal.get()));
            }
returnbackup;
        }

接着看restore()方法:

publicstaticvoidrestore(@NonNullObjectbackup) {
finalSnapshotbackupSnapshot= (Snapshot) backup;
restoreTtlValues(backupSnapshot.ttl2Value);
restoreThreadLocalValues(backupSnapshot.threadLocal2Value);
        }
privatestaticvoidrestoreTtlValues(@NonNullHashMap<TransmittableThreadLocal<Object>, Object>backup) {
// call afterExecute callbackdoExecuteCallback(false);
//以下打印语句 方面梳理整个工作流程 在源代码中不存在System.out.println("--------------------------------------------------------------");
System.out.println("restoreTtlValues开始,从holder变量里的获取数据列表");
inti=0;
for (Iterator<TransmittableThreadLocal<Object>>iterator=holder.get().keySet().iterator(); iterator.hasNext();){
TransmittableThreadLocal<Object>threadLocal=iterator.next();
i++;
System.out.println(String.format("从hold拿到的数据%s:key=%s, value=%s", i,threadLocal.hashCode(), threadLocal.get()));
            }
/*** 对holder再次遍历,然后做数据清洗工作* backup 也就是replay方法中对子线程原本持有的数据备份,而此时的hold变量,经过replay方法后,* 已经包括了某些主线程set进去的值* 所有需要借助backup 清理hold变量,使之恢复到子线程最之初,也就是被创建时携带的数据*/for (finalIterator<TransmittableThreadLocal<Object>>iterator=holder.get().keySet().iterator(); iterator.hasNext(); ) {
TransmittableThreadLocal<Object>threadLocal=iterator.next();
if (!backup.containsKey(threadLocal)) {
iterator.remove();
threadLocal.superRemove();
                }
            }
//以下打印语句 方面梳理整个工作流程 在源代码中不存在System.out.println("--------------------------------------------------------------");
System.out.println("restoreTtlValues结束,从holder变量里的获取数据列表");
i=0;
for (Iterator<TransmittableThreadLocal<Object>>iterator=holder.get().keySet().iterator(); iterator.hasNext();){
TransmittableThreadLocal<Object>threadLocal=iterator.next();
i++;
System.out.println(String.format("从hold拿到的数据%s:key=%s, value=%s", i,threadLocal.hashCode(), threadLocal.get()));
            }
/*** 1、根据backup备份数据,将hold数据复原* 2、根据backup备份数据,将子线程重新恢复原有数值*/setTtlValuesTo(backup);
        }

以上代码做个总结:在TtlRunnable的run方法中,先拿到captured快照数据,调用replay()方法,通过对hold变量遍历,对子线程原本数据对备份(backup),然后对比快照数据,清洗到hold变量中快照数据不存在的值。执行Runnable中的run方法,最后调用restore()方法,进行对hold变量和子线程持有数据进行恢复。

测试工作流程

核心代码已经结束了。为了更直观查看流程,从官网clone下来源代码,进行测试。

测试方法:

publicclassTtlControllerTest {
privatestaticExecutorServiceexecutorService=Executors.newFixedThreadPool(1);
publicstaticvoidmain(String[] args) throwsInterruptedException {
finalThreadLocaltl1=newTransmittableThreadLocal();
finalThreadLocaltl2=newTransmittableThreadLocal();
tl1.set(1);
executorService.execute(TtlRunnable.get(newRunnable() {
@Overridepublicvoidrun() {
try {
Thread.sleep(100L);
                } catch (InterruptedExceptione) {
e.printStackTrace();
                }
            }
        }));
newThread()
tl2.set(3);
Thread.sleep(1000L); //这块停顿一下executorService.execute(TtlRunnable.get(newRunnable() {
@Overridepublicvoidrun() {
try {
Thread.sleep(100L);
                } catch (InterruptedExceptione) {
e.printStackTrace();
                }
            }
        }));
    }
}

查看流程结果

main线程已成功在hold变量中存储值。key:1282788025,value=1----------------------------------------------------------------------------capture()开始从hold变量捕获数据从hold变量中拿到的数据1:key=1282788025, value=1main线程已成功在hold变量中存储值。key:2101440631,value=3--------------------------------------------------------------replayTtlValues开始,pool-1-thread-1线程从holder变量里的获取数据列表pool-1-thread-1线程从hold拿到的数据1:key=1282788025, value=1--------------------------------------------------------------replayTtlValues结束,pool-1-thread-1线程从holder变量里的获取数据列表pool-1-thread-1线程从hold拿到的数据1:key=1282788025, value=1--------------------------------------------------------------restoreTtlValues开始,从holder变量里的获取数据列表从hold拿到的数据1:key=1282788025, value=1--------------------------------------------------------------restoreTtlValues结束,从holder变量里的获取数据列表从hold拿到的数据1:key=1282788025, value=1----------------------------------------------------------------------------capture()开始从hold变量捕获数据从hold变量中拿到的数据1:key=2101440631, value=3从hold变量中拿到的数据2:key=1282788025, value=1--------------------------------------------------------------replayTtlValues开始,pool-1-thread-1线程从holder变量里的获取数据列表pool-1-thread-1线程从hold拿到的数据1:key=1282788025, value=1pool-1-thread-1线程已成功在hold变量中存储值。key:2101440631,value=3--------------------------------------------------------------replayTtlValues结束,pool-1-thread-1线程从holder变量里的获取数据列表pool-1-thread-1线程从hold拿到的数据1:key=2101440631, value=3pool-1-thread-1线程从hold拿到的数据2:key=1282788025, value=1--------------------------------------------------------------restoreTtlValues开始,从holder变量里的获取数据列表从hold拿到的数据1:key=2101440631, value=3从hold拿到的数据2:key=1282788025, value=1--------------------------------------------------------------restoreTtlValues结束,从holder变量里的获取数据列表从hold拿到的数据1:key=1282788025, value=1
相关文章
|
云计算
阿里云蔡英华:伙伴优先是阿里云的核心态度,没有之一
2022年7月13日,在2022阿里云合作伙伴大会上,阿里云智能全球销售总裁蔡英华发表了“坚持伙伴优先”主题演讲。他表示:“ 阿里云要恪守边界,与伙伴形成分工明确的合作模式、货真价实的权益体系和长期稳定的发展政策,并对伙伴收入占比提出明确数字要求。”
|
6月前
|
JavaScript Java 项目管理
基于Java的大学生创新创业项目管理系统设计与实现(亮点:完整严谨的创新创业申请流程、适用于任何要求严格的审批类毕业设计)
基于Java的大学生创新创业项目管理系统设计与实现(亮点:完整严谨的创新创业申请流程、适用于任何要求严格的审批类毕业设计)
238 0
|
6月前
|
运维 安全 Cloud Native
开源项目云化特别活动 | 寻找「云端工匠」
Gitee与阿里云计算巢合作推出开源项目孵化计划,旨在帮助开源项目实现商业化和用户增长。通过云计算巢,开发者可以便捷地部署、运维和交付软件,支持商业化交易。项目作者可扫描二维码或访问活动页面加入,完成任务将获得奖励,包括周边礼包和返现机会。此外,利用计算巢SaaS Boost,开发者能轻松定价和销售软件,获取更多收益。
|
5月前
|
SQL 存储 数据可视化
技术经验分享:ArcGISEngine开发之属性查询
技术经验分享:ArcGISEngine开发之属性查询
40 0
|
6月前
|
消息中间件 Java 关系型数据库
又一里程碑!阿里首推Java技术成长笔记,业内评级“钻石级”
根据数据表明,阿里巴巴已经连续3年获评最受欢迎的中国互联网公司,实际上阿里巴巴无论在科技创新力还是社会创造价值这几个方面,都是具有一定代表里的。在行业内,很多互联网企业也将阿里作为自己的标杆,越来越多的“打工人”也希望能够进到阿里工作。
|
前端开发 JavaScript 小程序
新来个技术总监,给公司项目引入了全新的业务架构,堪称最佳实践!
新来个技术总监,给公司项目引入了全新的业务架构,堪称最佳实践!
|
设计模式 XML 安全
【开源项目】银行查询服务的设计和实现
银行查询服务的设计初衷是:为提供更加便利的查询服务,我们在分布式系统架构下,独立开发了与各大银行对接的查询服务。该独立服务支持用户轻松查询账户余额和消费明细的信息,同时保证用户消费的可见性。这种架构设计,不仅提升了用户的查询体验、保证了用户的信息安全,更为整个分布式系统的性能和可维护性提供了保障,为用户和第三方支付机构的长期合作奠定了良好的基础。该服务设计以微服务为基础,使用多种设计模式。
188 0
【开源项目】银行查询服务的设计和实现
|
数据采集 人工智能 运维
|
SQL Python
热饭的测开成果盘点第四期:sql共享平台
本期介绍的是一个django平台,它是我在18年时做的一个sql共享平台,整个最大的亮点我觉得就是的优美风格的尝试。
热饭的测开成果盘点第四期:sql共享平台
|
消息中间件 机器人 RocketMQ
【通用行业开发部】记一次rocketMq的踩坑经历
遇到的极小众问题:阿里云RocketMq的死分区与假堆积
下一篇
无影云桌面