当前位置: 首页 > 后端技术 > Java

你知道ThreadLocal吗?说说有什么用

时间:2023-04-01 15:42:56 Java

摘要:ThreadLocal是java提供的一个类,用来在本线程的不同方法中传递和获取方便的对象。它定义的变量只在本线程可见和维护,不受其他线程影响,与其他线程隔离。本文分享自华为云社区《ThreadLocal:线程专属的变量》,作者:左泽伟。ThreadLocal简介ThreadLocal是java提供的一个类,用来方便本线程中不同方法中对象的传递和获取。它定义的变量只在本线程可见和维护,不受其他线程影响,与其他线程隔离。那么ThreadLocal解决什么问题,适用于什么样的场景呢?此类提供线程局部变量。这些变量与它们的普通对应变量的不同之处在于,每个访问一个变量(通过其get或set方法)的线程都有其自己的、独立初始化的变量副本。ThreadLocal实例通常是希望将状态与线程相关联的类中的私有静态字段(例如,用户ID或事务ID)。只要线程处于活动状态并且ThreadLocal实例可访问,每个线程都持有对其线程局部变量副本的隐式引用;在一个线程消失后,它的所有线程本地实例的副本都将被垃圾收集(除非存在对这些副本的其他引用)。核心意思就是ThreadLocal提供了线程局部的实例。它与普通变量的不同之处在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal变量通常用privatestatic修饰。当一个线程终止时,它使用的所有ThreadLocal相关实例副本都可以被回收。总的来说,ThreadLocal适用于每个线程都需要自己独立的实例,并且该实例需要在多个方法中使用的场景,即变量在线程之间隔离,在方法或类之间共享。这个观点后面会举例详细说明。另外,在这种场景下,没有必要使用ThreadLocal,其他方法也可以达到同样的效果,只是ThreadLocal让实现更加简洁。ThreadLocal使用ThreadLocal通过set方法给变量赋值,通过get方法获取变量的值。当然也可以在定义变量的时候通过ThreadLocal.withInitial方法给变量赋初值,或者定义一个继承ThreadLocal的类,然后重写initialValue方法。下面通过如下代码说明ThreadLocal的使用方式:publicclassTestThreadLocal{privatestaticThreadLocalbuilder=ThreadLocal.withInitial(StringBuilder::new);publicstaticvoidmain(String[]args){for(inti=0;i<5;i++){newThread(()->{StringthreadName=Thread.currentThread().getName();for(intj=0;j<3;j++){append(j);System.out.printf("%sappend%d,nowbuildervalueis%s,ThreadLocalinstancehashcodeis%d,ThreadLocalinstancemappingvaluehashcodeis%d\n",threadName,j,builder.get().toString(),builder.hashCode(),builder.get().hashCode());}change();System.out.printf("%sset新的stringbuilder,现在builder值为%s,ThreadLocal实例hashcode为%d,ThreadLocal实例映射值hashcode为%d\n",threadName,builder.get().toString(),builder.hashCode(),builder.get().hashCode());},"thread-"+i).start();}}privatestaticvoidappend(intnum){builder.get().append(num);}privatestaticvoidchange(){StringBuildernewStringBuilder=newStringBuilder("HelloWorld");builder.set(newStringBuilder);}}例子中定义了一个builder的ThreadLocal对象,然后5个线程分别访问和修改builder对象。这两个操作是在两个不同的函数append和change中进行的。两个函数对builder对象的访问也是直接获取,而不是传入函数的入参。进来代码输出如下:thread-0append0,nowbuildervalueis0,ThreadLocalinstancehashcodeis796465865,ThreadLocalinstancemappingvaluehashcodeis566157654thread-0append1,nowbuildervalueis01,ThreadLocalinstancehashcodeis796465865,ThreadLocal实例映射值hashcode为566157654thread-4append0,现在builder值为0,ThreadLocal实例hashcode为796465865,ThreadLocal实例映射值hashcode为654647086thread-3append0,现在builder值为0,ThreadLocal实例hashcode为796465865,ThreadLocal实例映射值hashcode为1803363945thread-2append0,现在builder值为0,ThreadLocal实例hashcode为796465865,ThreadLocal实例映射值hashcode为1535812498thread-1append0,现在builder值为0,ThreadLocal实例hashcode为796465865,ThreadLocal实例映射值hashcode为2075237830thread-2append1,现在builder值为01,ThreadLocal实例hashcode为796465865,ThreadLocal实例映射值hashcode为1535812498thread-3append1,现在builder值为01,ThreadLocal实例hashcode为796465865,ThreadLocal实例映射值hashcode为1803363945thread-4append1,现在builder值为01,ThreadLocal实例hashcode为796465865,ThreadLocal实例映射值hashcode为654647086thread-0append2,现在builder值为012,ThreadLocal实例hashcode为796465865,ThreadLocal实例映射值hashcode为566157654thread-0设置新的stringbuilder,现在builder值为HelloWorld,ThreadLocal实例hashcode为796465865,ThreadLocal实例映射valuehashcode为1773033190thread-4append2,现在builder值为012,ThreadLocal实例hashcode为796465865,ThreadLocal实例映射值hashcode为654647086thread-4设置新的stringbuilder,现在builder值为HelloWorld,ThreadLocal实例hashcode为796465865,ThreadLocal实例映射值哈希码是700642750thread-3append2,现在builder值为012,ThreadLocal实例hashcode为796465865,ThreadLocal实例映射值hashcode为1803363945thread-3设置新的stringbuilder,现在builder值为HelloWorld,ThreadLocal实例hashcode为796465865,ThreadLocal实例映射值hashcode为1706743158thread-2append2,现在builder值为012,ThreadLocal实例hashcode为796465865,ThreadLocal实例映射值hashcode为1535812498thread-2setnewstringbuilder,现在builder值为HelloWorld,ThreadLocal实例hashcode为796465865,ThreadLocal实例映射值hashcode为1431127699thread-1append1,现在builder值为01,ThreadLocal实例hashcode为796465865,ThreadLocal实例映射值hashcode为2075237830thread-1append2,现在builder值为012,ThreadLocal实例hashcode为796465865,ThreadLocal实例映射值hashcode为2075237830thread-1set新的stringbuilder,现在构建er值为HelloWorld,ThreadLocal实例hashcode为796465865,ThreadLocal实例映射值hashcode为1970695360从输出中的第1行到第6行可以看出不同的线程访问同一个builder对象(ThreadLocal实例hashcode值输出不同线程相同),但每个线程获取的builder对象中存储的实例StringBuilder不同(不同线程输出的ThreadLocal实例映射值hashcode值不同)。从输出中的1~2行和9~10行可以看出,同一个线程修改builder对象存储的实例的值,不会影响其他线程的builder对象存储的实例(thread-4线程修改存储的StringBuilder的值,并且不会导致thread-0线程的ThreadLocal实例映射值hashcode值改变)从输出中的第9行到第13行可以看出,当一个线程改变了ThreadLocal对象存储的值,不会影响其他线程(thread-0在本线程调用set方法改变了ThreadLocal存储的对象的值,并且本线程thread-4的ThreadLocal实例映射值hashcode发生了改变,但是ThreadLocal实例映射thread-4的值hashcode没有改变)。ThreadLocal的原理ThreadLocal可以隔离各个线程,主要是通过在每个Thread对象中维护一个ThreadLocalMap来实现的。因为是一个线程中的对象,所以对其他线程是不可见的,从而达到隔离的目的。那为什么是Map结构呢?主要原因是一个线程中可能存在多个ThreadLocal对象,需要一个集合进行存储区分,使用Map可以更快的找到相关对象。ThreadLocalMap是ThreadLocal对象的一个??静态内部类,内部维护了一个Entry数组,用于实现Map的get、put等操作。为了简单起见,可以看成是一个Map,其中key是一个ThreadLocal实例,value是一个ThreadLocal实例对象存储的值。ThreadLocal的适用场景上面提到了ThreadLocal适用于以下场景:每个线程都需要有自己独立的实例,比如实现每个线程单例类或者每个线程的上下文信息(比如事务ID)。ThreadLocal适用于变量在线程间隔离,方法间共享的场景,提供了另一种扩展Thread的方式。如果要保留或将信息从一个方法调用传递到另一个方法调用,则可以使用ThreadLocal来实现。这允许很大的灵活性,因为不需要修改任何方法。Case1这里有一个处理flags的类,可以通过ThreadLocal来保证每个请求都有一个唯一的trackingflag。公共类TestFlagHolder{privatefinalstaticThreadLocalTEST_FLAG=newThreadLocal<>();publicstaticvoidset(Stringvalue){TEST_FLAG.set(value);}publicstaticStringget(){returnTEST_FLAG.get();}publicstaticStringget4log(){if(TEST_FLAG.get()==null){return"-";}返回TEST_FLAG.get();}publicstaticvoidremove(){TEST_FLAG.remove();}}案例2同一线程中trace信息的传递:ThreadLocaltraceContext=newThreadLocal<>();StringtraceId=Tracer.startServer();traceContext.set(traceId)//生成trace信息传递给线程本地...Tracer.startClient(traceContext.get());//从线程本地获取跟踪信息Tracer.endClient();...Tracer.endServer();Case3在同一个请求的每一行日志中添加相同的标记。这样,只要拿到这个标记,就可以在请求链接上查询所有步骤的耗时了。我们称这个标记为requestId。我们可以在程序入口生成一个requestId,然后放到线程上下文中,这样需要的时候就可以随时从线程上下文中获取requestId。一个简单的代码实现如下:StringrequestId=UUID.randomUUID().toString();ThreadLocaltl=newThreadLocal(){@OverrideprotectedStringinitialValue(){}};//requestId保存在线程上下文中-开始));//添加requestIdstart=System.currentTimeMillis();processB();Logs.info("rid:"+tl.get()+",processBcost"+(System.currentTimeMillis()-start));start=System.currentTimeMillis();processC();Logs.info("rid:"+tl.get()+",processCcost"+(System.currentTimeMillis()-start));是的知道了requestId,就可以清楚的知道一个调用链路上的耗时分布。总结无论是单体系统还是微服务架构,无论是链接标记还是服务跟踪,都需要在系统中加入TraceId,让你的链接串起来,呈现给你一个完整的问题场景。如果可以在客户端生成TraceId,在请求业务接口时传递给服务端,那么也可以集成客户端的日志系统,更有利于排查问题。同时,在全链路压测框架中,Trace信息的传递功能是基于ThreadLocal的。但是在实际业务中可能会使用异步调用,这样会丢失Trace信息,破坏链路的完整性。因此,建议大家在实际项目中不要轻易使用ThreadLocal。参考资料:[1]:《高并发系统设计40问》[2]:https://juejin.cn/post/684490...点击关注,第一时间了解华为云的新鲜技术~