找回密码
 会员注册
查看: 24|回复: 0

实战指南:理解ThreadLocal原理并用于Java多线程上下文管理

[复制链接]

3

主题

0

回帖

10

积分

新手上路

积分
10
发表于 2024-9-11 18:59:15 | 显示全部楼层 |阅读模式
目录一、ThreadLocal基本知识回顾分析(一)ThreadLocal原理(二)既然ThreadLocalMap的key是弱引用,GC之后key是否为null?(三)ThreadLocal中的内存泄漏问题及JDK处理方法(四)部分核心源码回顾ThreadLocal.set()方法源码详解ThreadLocalMap.get()方法详解ThreadLocal.remove()方法源码详解(五)简单的直观体会二、基于Threadlocal实现的上下文管理组件ContextManager(一)定义ContextManager类(二)使用ContextManager进行上下文管理(三)扩展ContextManager的使用方式三、在线程池中传递ContextManager(一)增加静态方法,用于在已有的上下文中执行任务(二)自定义线程池实现(三)测试自定义线程池四、总结干货分享,感谢您的阅读!探讨如何基于ThreadLocal实现一个高效的上下文管理组件,以解决多线程环境下的数据共享和上下文管理这些问题。通过具体的代码示例和实战展示ThreadLocal如何为多线程编程提供一种简洁而高效的上下文管理方案。一、ThreadLocal基本知识回顾分析(一)ThreadLocal原理ThreadLocal是Java提供的一个用于线程级别数据存储的类。它为每个线程提供了独立的变量副本,使得每个线程都能独立地操作自己的变量,而不会与其他线程的变量冲突。这种机制特别适用于需要线程隔离的场景,通过ThreadLocal,我们可以确保同一个变量在不同线程中拥有各自独立的值。我们先来看下Thread、ThreadLocalMap、ThreadLocal结构关系:每个Thread都有一个ThreadLocalMap变量ThreadLocalMap内部定义了Entry(ThreadLocalk,Objectv)节点类,这个节点继承了WeakReference类泛型为ThreacLocal类ThreadLocal主要作用就是实现线程间变量隔离,对于一个变量,每个线程维护一个自己的实例,防止多线程环境下的资源竞争,那ThreadLocal是如何实现这一特性的呢?基本原理实现如下:每个Thread对象中都包含一个ThreadLocal.ThreadLocalMap类型的threadlocals成员变量;该map对应的每个元素Entry对象中:key是ThreadLocal对象的弱引用,value是该threadlocal变量在当前线程中的对应的变量实体;当某一线程执行获取该ThreadLocal对象对应的变量时,首先从当前线程对象中获取对应的threadlocals哈希表,再以该ThreadLocal对象为key查询哈希表中对应的value;由于每个线程独占一个threadlocals哈希表,因此线程间ThreadLocal对象对应的变量实体也是独占的,不存在竞争问题,也就避免了多线程问题。(二)既然ThreadLocalMap的key是弱引用,GC之后key是否为null?在搞清楚这个问题之前,我们需要先搞清楚Java的四种引用类型:强引用:new出来的对象就是强引用,只要强引用存在,垃圾回收器就永远不会回收被引用的对象,哪怕内存不足的时候。软引用:使用SoftReference修饰的对象被称为软引用,在内存要溢出的时候软引用指向的对象会被回收。弱引用:使用WeakReference修饰的对象被称为弱引用,只要发生垃圾回收,被弱引用指向的对象就会被回收。虚引用:虚引用是最弱的引用,用PhantomReference进行定。唯一的作用就是用来队列接受对象即将死亡的通知。这个问题的答案是不为null,从上图的图示就可以直接看出。(三)ThreadLocal中的内存泄漏问题及JDK处理方法由图可知,ThreadLocal.ThreadLocalMap 对应的Entry中,key为ThreadLocal对象的弱引用,方法执行对应栈帧中的ThreadLocal引用为强引用。当方法执行过程中,由于栈帧销毁或者主动释放等原因,释放了ThreadLocal对象的强引用,即表示该ThreadLocal对象可以被回收了。又因为Entry中key为ThreadLocal对象的弱引用,所以当jvm执行GC操作时是能够回收该ThreadLocal对象的。而Entry中value对应的是变量实体对象的强引用,因此释放一个ThreadLocal对象,是无法释放ThreadLocal.ThreadLocalMap中对应的value对象的,也就造成了内存泄漏。除非释放当前线程对象,这样整个threadlocals都被回收了。但是日常开发中会经常使用线程池等线程池化技术,释放线程对象的条件往往无法达到。JDK处理的方法是,在ThreadLocalMap进行set()、get()、remove()的时候,都会进行清理:privateEntrygetEntry(ThreadLocalkey){inti=key.threadLocalHashCode&(table.length-1);Entrye=table[i];if(e!=null&e.get()==key)returne;elsereturngetEntryAfterMiss(key,i,e);}privateEntrygetEntryAfterMiss(ThreadLocalkey,inti,Entrye){Entry[]tab=table;intlen=tab.length;while(e!=null){ThreadLocalk=e.get();if(k==key)returne;if(k==null)//如果key为null,对应的threadlocal对象已经被回收,清理该EntryexpungeStaleEntry(i);elsei=nextIndex(i,len);e=tab[i];}returnnull;}(四)部分核心源码回顾ThreadLocal的API很少就包含了4个,分别是get()、set()、remove()、withInitial(),源码如下:publicTget(){}publicvoidset(Tvalue){}publicvoidremove(){}publicstaticThreadLocalwithInitial(Suppliersupplier){}get():从当前线程的ThreadLocalMap获取与当前ThreadLocal对象对应的值。如果ThreadLocalMap中不存在该值,则调用setInitialValue()方法进行初始化。set(Tvalue):将当前线程的ThreadLocalMap中的值设置为给定的value。如果当前线程没有ThreadLocalMap,则会创建一个新的ThreadLocalMap并将值设置进去。remove():从当前线程的ThreadLocalMap中移除与当前ThreadLocal对象对应的值,帮助防止内存泄漏。withInitial(Suppliersupplier):返回一个新的ThreadLocal对象,其初始值由Supplier提供。这允许使用者在创建ThreadLocal时指定初始值。针对这几个源码我们重点进行分析和体会。ThreadLocal.set()方法源码详解pubicvoidset(Tvalue){//获取当前线程Threadt=Threac.currentThread();//获取当前线程的ThreadLocalMapThreadLocalMapmap=getMap(t);//如果map不为null,调用ThreadLocalMap.set()方法设置值if(map!=null)map.set(this,value);else//map为null,调用createMap()方法初始化创建mapcreateMap(t,value);}//返回线程的ThreadLocalMap.threadLocalsThreadLocalMapgetMap(Threadt){returnt.threadLocals;}//调用ThreadLocalMap构造方法创建ThreadLocalMapvoidcreateMap(Threadt,TfirstValue){t.threadLocals=newThreadLocalMap(this,firstValue);}//ThreadLocalMap构造方法,传入firstKey,firstValueThreadLocalMap(ThreadLocalfirstKey,ObjectfirstValue){//初始化Entry表的容量=16table=newEntry[INITIAL_CAPACITY];//获取ThreadLocal的hashCode值与运算得到数组下标inti=firsetKey.threadLocalHashCode&(INITAL_CAPACITY-1);//通过下标Entry表赋值table[i]=newEntry(firstKey,firstValue);//Entry表存储元素数量初始化为1size=1;//设置Entry表扩容阙值默认为len*2/3setThreshold(INITIAL_CAPACITY);}privatevoidsetThreshold(intlen){threshold=len*2/3}ThreadLocal.set()方法还是很简单的,核心方法在ThreadLocalMap.set()方法基本流程可总结如下:ThreadLocalMap.get()方法详解publicTget(){Threadt=Thread.currentThread();ThreadLocalMapmap=getMap(t);if(map!=null){ThreadLocalMap.Entrye=map.getEntry(this);if(e!=null){@SuppressWarnings("unchecked")Tresult=(T)e.value;returnresult;}}//未找到的话,则调用setInitialValue()方法设置nullreturnsetInitialValue();}privateEntrygetEntry(ThreadLocalkey){inti=key.threadLocalHashCode&(table.length-1);Entrye=table[i];//key相等直接返回if(e!=null&e.get()==key)returne;else//key不相等调用getEntryAfterMiss()方法returngetEntryAfterMiss(key,i,e);}privateEntrygetEntryAfterMiss(ThreadLocalkey,inti,Entrye){Entry[]tab=table;intlen=tab.length;//迭代往后查找key相等的entrywhile(e!=null){ThreadLocalk=e.get();if(k==key)returne;//遇到key=null的entry,先进行探测式清理工作if(k==null)expungeStaleEntry(i);elsei=nextIndex(i,len);e=tab[i];}returnnull;}主要包含两种情况,一种是hash计算出下标,该下标对应的Entry.key和我们传入的key相等的情况,另外一种就是不相等的情况。相等情况:相等情况处理很简单,直接返回value,如下图,比如get(ThreadLocal1)计算下标为4,且4存在Entry,且key相等,则直接返回value=11:不相等情况:不相等情况,以get(ThreadLocal2)为例计算下标为4,且4存在Entry,但key相等,这个时候则为往后迭代寻找key相等的元素,如果寻找过程中发现了有key=null的元素则回进行探测式清理操作。如下图:迭代到index=5的数据时,此时Entry.key=null,触发一次探测式数据回收操作,执行expungeStaleEntry()方法,执行完后,index5、8的数据都会被回收,而index6、7的数据都会前移,此时继续往后迭代,到index=6的时候即找到了key值相等的Entry数据,如下图:ThreadLocal.remove()方法源码详解publicvoidremove(){//获取当前线程的ThreadLocalMapThreadLocalMapm=getMap(Thread.currentThread());if(m!=null)//如果当前线程有ThreadLocalMap,则在map中移除当前ThreadLocal的值m.remove(this);}staticclassThreadLocalMap{//内部Entry类,继承自WeakReference>staticclassEntryextendsWeakReference>{//ThreadLocal对应的值Objectvalue;Entry(ThreadLocalk,Objectv){super(k);value=v;}}//线程局部变量哈希表privateEntry[]table;privatevoidremove(ThreadLocalkey){Entry[]tab=table;intlen=tab.length;//计算当前ThreadLocal的哈希值在数组中的索引位置inti=key.threadLocalHashCode&(len-1);//从hash获取的下标开始,寻找key相等的entry元素清除for(Entrye=tab[i];e!=null;e=tab[i=nextIndex(i,len)]){if(e.get()==key){e.clear();//清除键的引用expungeStaleEntry(i);//清除相应的值return;}}}//用于计算下一个索引位置privateintnextIndex(inti,intlen){return((i+1k=e.get();if(k==null){e.value=null;tab[i]=null;}else{inth=k.threadLocalHashCode&(len-1);if(h!=i){tab[i]=null;while(tab[h]!=null)h=nextIndex(h,len);tab[h]=e;}}}}}ThreadLocal.remove()核心是调用ThreadLocalMap.remove()方法,流程如下:通过hash计算下标。从散列表该下标开始往后查key相等的元素,如果找到则做清除操作,引用置为null,GC的时候key就会置为null,然后执行探测式清理处理。(五)简单的直观体会以下是ThreadLocal的基本使用示例:packageorg.zyf.javabasic.thread.threadLocal;/***@program:zyfboot-javabasic*@description:ThreadLocal的基本使用示例*@author:zhangyanfeng*@create:2024-06-0213:22**/publicclassThreadLocalExample{privatestaticThreadLocalthreadLocal=ThreadLocal.withInitial(()->1);publicstaticvoidmain(String[]args){Runnabletask=()->{intvalue=threadLocal.get();System.out.println(Thread.currentThread().getName()+"initialvalue:"+value);threadLocal.set(value+1);System.out.println(Thread.currentThread().getName()+"updatedvalue:"+threadLocal.get());};Threadthread1=newThread(task,"Thread1");Threadthread2=newThread(task,"Thread2");thread1.start();thread2.start();}}直接结果查看可感受到其ThreadLocal主要作用就是实现线程间变量隔离,对于一个变量,每个线程维护一个自己的实例,防止多线程环境下的资源竞争。二、基于Threadlocal实现的上下文管理组件ContextManager在实际开发中,我们经常需要维护一些上下文信息,这样可以避免在方法调用过程中传递过多的参数。例如,当Web服务器收到一个请求时,需要解析当前登录状态的用户,并在后续的业务处理中使用这个用户名。如果只需要维护一个上下文数据,如用户名,可以通过方法传参的方式,将用户名作为参数传递给每个业务方法。然而,如果需要维护的上下文信息较多,这种方式就显得笨拙且难以维护。一个更加优雅的解决方案是使用ThreadLocal来实现请求线程的上下文管理。这样,同一线程中的所有方法都可以通过ThreadLocal对象直接读取和修改上下文信息,而无需在方法间传递参数。当需要维护多个上下文状态时,可以使用多个ThreadLocal实例来存储不同的信息。虽然这种方式在某些情况下也能接受,但在使用线程池时,问题就变得复杂了。因为线程池中的线程会被多个请求重复使用,如何将ThreadLocal中的上下文信息从主线程传递到线程池中的工作线程成为一个难题。基于上述考虑,我们介绍一种基于ThreadLocal实现的上下文管理组件ContextManager,它能够简化上下文信息的管理,并解决线程池环境中的上下文传递问题。(一)定义ContextManager类首先,定义一个ContextManager类用于管理上下文信息。packageorg.zyf.javabasic.thread.threadLocal;importjava.util.concurrent.ConcurrentHashMap;importjava.util.concurrent.ConcurrentMap;/***@program:zyfboot-javabasic*@description:用于管理上下文信息*@author:zhangyanfeng*@create:2024-06-0213:48**/publicclassContextManager{//静态变量,维护不同线程的上下文privatestaticfinalThreadLocalCONTEXT_THREAD_LOCAL=newThreadLocal();//实例变量,维护每个上下文中所有的状态数据privatefinalConcurrentMapvalues=newConcurrentHashMap();//获取当前线程的上下文publicstaticContextManagergetCurrentContext(){returnCONTEXT_THREAD_LOCAL.get();}//在当前上下文设置一个状态数据publicvoidset(Stringkey,Objectvalue){if(value!=null){values.put(key,value);}else{values.remove(key);}}//在当前上下文读取一个状态数据publicObjectget(Stringkey){returnvalues.get(key);}//开启一个新的上下文publicstaticContextManagerbeginContext(){ContextManagercontext=CONTEXT_THREAD_LOCAL.get();if(context!=null){thrownewIllegalStateException("Acontextisalreadystartedinthecurrentthread.");}context=newContextManager();CONTEXT_THREAD_LOCAL.set(context);returncontext;}//关闭当前上下文publicstaticvoidendContext(){CONTEXT_THREAD_LOCAL.remove();}}(二)使用ContextManager进行上下文管理假设我们有一个在线商城系统,用户在进行购物时需要进行身份认证,并且在用户进行购物操作时,需要记录用户的购物车信息。我们可以使用ContextManager类来管理用户的上下文信息。packageorg.zyf.javabasic.thread.threadLocal;importorg.zyf.javabasic.skills.reflection.dto.Product;/***@program:zyfboot-javabasic*@description:用户在进行购物时需要进行身份认证,并且在用户进行购物操作时,需要记录用户的购物车信息。*@author:zhangyanfeng*@create:2024-06-0214:02**/publicclassShoppingCartService{publicvoidaddToCart(Productproduct,intquantity){//开启一个新的上下文ContextManager.beginContext();try{//将用户ID和商品信息设置到当前上下文中ContextManager.getCurrentContext().set("userId",getCurrentUserId());ContextManager.getCurrentContext().set("product",product);ContextManager.getCurrentContext().set("quantity",quantity);//执行添加到购物车的逻辑//这里可以调用其他方法,或者执行其他操作System.out.println("Addingproducttocart...");checkout();}finally{//关闭当前上下文ContextManager.endContext();}}publicvoidcheckout(){//从当前上下文中读取用户ID和购物车信息StringuserId=(String)ContextManager.getCurrentContext().get("userId")roductproduct=(Product)ContextManager.getCurrentContext().get("product");intquantity=(int)ContextManager.getCurrentContext().get("quantity");//执行结账逻辑//这里可以根据购物车信息进行结账操作System.out.println("Checkingout...");System.out.println("UserID:"+userId);System.out.println("Product:"+product.getName());System.out.println("Quantity:"+quantity);}privateStringgetCurrentUserId(){//模拟获取当前用户ID的方法return"user123";}publicstaticvoidmain(String[]args){ShoppingCartServiceshoppingCartService=newShoppingCartService()roductproduct=newProduct();product.setName("iPhone");product.setId(1000);shoppingCartService.addToCart(product,1);}}在这个示例中,ShoppingCartService类模拟了一个购物车服务。在addToCart()方法中,我们开启了一个新的上下文,并将当前用户ID、商品信息和购买数量设置到上下文中。在checkout()方法中,我们从当前上下文中读取了用户ID、商品信息和购买数量,并执行了结账操作。通过使用ContextManager类,我们可以轻松地在购物车服务中管理用户的上下文信息,而无需手动传递参数。(三)扩展ContextManager的使用方式我们可以给ContextManager添加类似的静态方法,以简化代码的书写。当前请视业务情况进行应用和分析。packageorg.zyf.javabasic.thread.threadLocal;importjava.util.concurrent.ConcurrentHashMap;importjava.util.concurrent.ConcurrentMap;importjava.util.function.Supplier;/***@program:zyfboot-javabasic*@description:用于管理上下文信息*@author:zhangyanfeng*@create:2024-06-0213:48**/publicclassContextManager{//其他省去//执行带有新的上下文的任务publicstaticvoidrunWithNewContext(Runnabletask)throwsX{beginContext();try{task.run();}finally{endContext();}}//在新的上下文中执行任务,并返回结果publicstaticTsupplyWithNewContext(Suppliersupplier)throwsX{beginContext();try{returnsupplier.get();}finally{endContext();}}}三、在线程池中传递ContextManager我们通过ThreadLocal实现了一个自定义的上下文管理组件ContextManager,并通过ContextManager.set()和ContextManager.get()方法在同一个线程中读写上下文中的状态数据。现在,我们需要扩展这个功能,使其在一个线程执行过程中开启了一个ContextManager,随后使用线程池执行任务时,也能获取到当前ContextManager中的状态数据。这在如下场景中很常见:服务收到一个用户请求,通过ContextManager将登录态数据存储到当前线程的上下文中,随后使用线程池执行一些耗时操作,并希望线程池中的线程也能访问这些登录态数据。由于线程池中的线程和请求线程不是同一个线程,按照目前的实现,线程池中的线程无法访问请求线程的上下文数据。为了解决这个问题,我们可以在提交Runnable时,将当前的ContextManager引用存储在Runnable对象中。当线程池中的线程开始执行时,将ContextManager替换到执行线程的上下文中,执行完成后再恢复原来的上下文。(一)增加静态方法,用于在已有的上下文中执行任务首先,添加静态方法runWithExistingContext和supplyWithExistingContext,用于在指定的上下文中执行任务:packageorg.zyf.javabasic.thread.threadLocal;importjava.util.concurrent.ConcurrentHashMap;importjava.util.concurrent.ConcurrentMap;importjava.util.function.Supplier;/***@program:zyfboot-javabasic*@description:用于管理上下文信息*@author:zhangyanfeng*@create:2024-06-0213:48**/publicclassContextManager{//省略publicstaticvoidrunWithExistingContext(ContextManagercontext,Runnabletask)throwsX{supplyWithExistingContext(context,()->{task.run();returnnull;});}publicstaticTsupplyWithExistingContext(ContextManagercontext,Suppliersupplier)throwsX{ContextManageroldContext=CONTEXT_THREAD_LOCAL.get();CONTEXT_THREAD_LOCAL.set(context);try{returnsupplier.get();}finally{if(oldContext!=null){CONTEXT_THREAD_LOCAL.set(oldContext);}else{CONTEXT_THREAD_LOCAL.remove();}}}}(二)自定义线程池实现创建一个自定义线程池ContextAwareThreadPoolExecutor,确保任务在执行时可以正确传递和恢复上下文信息:packageorg.zyf.javabasic.thread.threadLocal;importjava.util.concurrent.BlockingQueue;importjava.util.concurrent.LinkedBlockingQueue;importjava.util.concurrent.ThreadPoolExecutor;importjava.util.concurrent.TimeUnit;importstaticorg.zyf.javabasic.thread.threadLocal.ContextManager.runWithExistingContext;/***@program:zyfboot-javabasic*@description:自定义线程池ContextAwareThreadPoolExecutor*@author:zhangyanfeng*@create:2024-06-0220:23**/publicclassContextAwareThreadPoolExecutorextendsThreadPoolExecutor{publicContextAwareThreadPoolExecutor(intcorePoolSize,intmaximumPoolSize,longkeepAliveTime,TimeUnitunit,BlockingQueueworkQueue){super(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue);}publicstaticContextAwareThreadPoolExecutornewFixedThreadPool(intnThreads){returnnewContextAwareThreadPoolExecutor(nThreads,nThreads,0L,TimeUnit.MILLISECONDS,newLinkedBlockingQueue());}@Overridepublicvoidexecute(Runnablecommand){ContextManagercontext=ContextManager.getCurrentContext();super.execute(()->runWithExistingContext(context,command::run));}}(三)测试自定义线程池验证ContextAwareThreadPoolExecutor是否正确传递和恢复上下文:packageorg.zyf.javabasic.thread.threadLocal;importorg.junit.Test;importjava.util.concurrent.ExecutorService;/***@program:zyfboot-javabasic*@description:验证ContextAwareThreadPoolExecutor是否正确传递和恢复上下文*@author:zhangyanfeng*@create:2024-06-0220:25**/publicclassContextManagerTest{@TestpublicvoidtestContextAwareThreadPoolExecutor(){ContextManager.beginContext();try{ContextManager.getCurrentContext().set("key","valueoutofthreadpool");Runnabler=()->{Stringvalue=(String)ContextManager.getCurrentContext().get("key");System.out.println("Valueinthreadpool:"+value);};ExecutorServiceexecutor=ContextAwareThreadPoolExecutor.newFixedThreadPool(10);executor.execute(r);executor.submit(r);}finally{ContextManager.endContext();}/**执行结果*Valueinthreadpool:valueoutofthreadpool*Valueinthreadpool:valueoutofthreadpool*/}@TestpublicvoidtestContextAwareThreadPoolExecutorWithNewContext(){ContextManager.runWithNewContext(()->{ContextManager.getCurrentContext().set("key","valueoutofthreadpool");Runnabler=()->{Stringvalue=(String)ContextManager.getCurrentContext().get("key");System.out.println("Valueinthreadpool:"+value);};ExecutorServiceexecutor=ContextAwareThreadPoolExecutor.newFixedThreadPool(10);executor.execute(r);executor.submit(r);});/**执行结果*Valueinthreadpool:valueoutofthreadpool*Valueinthreadpool:valueoutofthreadpool*/}}验证ContextAwareThreadPoolExecutor是否能正确传递和恢复上下文信息。测试用例涵盖了两种情况:在当前上下文中执行任务,并使用自定义线程池执行任务。在新的上下文中执行任务,并使用自定义线程池执行任务。这两种情况覆盖了在不同上下文环境中使用线程池的情况,确保了上下文信息能够正确传递和恢复。因此,验证内容是完备的,没有问题。四、总结探讨如何基于ThreadLocal实现一个高效的上下文管理组件,以解决多线程环境下的数据共享和上下文管理这些问题。通过具体的代码示例和实战展示ThreadLocal如何为多线程编程提供一种简洁而高效的上下文管理方案。参考文章https://www.cnblogs.com/wupeixuan/p/12638203.html一张图看懂Java中的ThreadLocal原理_threadlocal原理图解-CSDN博客ThreadLocal原理·进击的java菜鸟一文搞懂ThreadLocal原理-51CTO.COM滑动验证页面基于ThreadLocal实现一个上下文管理组件(附源码)
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-27 14:13 , Processed in 0.414531 second(s), 26 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表