当前位置: 首页 > 科技观察

你知道这4个ThreadLocal吗?

时间:2023-03-13 20:24:52 科技观察

什么是ThreadLocal?顾名思义,ThreadLocal类可以理解为线程局部变量。也就是说如果定义了一个ThreadLocal,那么各个线程对这个ThreadLocal的读写都是线程隔离的,不会互相影响。它提供了一种通过传递可变数据来实现线程关闭的机制,每个线程都有自己独立的副本。在实际应用和实际开发中,我们真正使用ThreadLocal的场景还是比较少的,大部分都是在框架中使用。最常见的使用场景就是用它来解决数据库连接,Session管理等,保证每个线程使用的数据库连接是一样的。另外一个用的比较多的场景是解决SimpleDateFormat解决线程不安全的问题,不过现在java8提供了线程安全的DateTimeFormatter,有兴趣的同学可以去看看。它还可以用于优雅地传递参数。传递参数时,如果将父线程产生的变量或参数直接通过ThreadLocal传递给子线程参数,则参数会丢失。这个后面会介绍另外一个ThreadLocal来专门解决这个问题。ThreadLocalapi介绍ThreadLocal的API还是比较少的,先来看看这些api的使用,使用起来超级简单privatestaticThreadLocalthreadLocal=ThreadLocal.withInitial(()->"javafinance");publicstaticvoidmain(String[]args){System.out.println("获取初始值:"+threadLocal.get());threadLocal.set("注意:[java金融]");System.out.println("获取修改的最终值:“+threadLocal.get());threadLocal.remove();}输出结果:获取初始值:JavaFinance获取修改后的值:注意:【JavaFinance】炸鸡简单,几行代码就涵盖了所有API。我们先简单看一下这些API的源码。成员变量/**初始容量,必须是2的幂*Theinitialcapacity--MUSTbeapoweroftwo.*/privatestaticfinalintINITIAL_CAPACITY=16;/**表项,大小必须是2*Thetable的幂,resizedasnecessary.*table.lengthMUSTalwaysbeapoweroftwo.*/privateEntry[]table;/***Thenumberofentriesinthetable.*/privateintsize=0;/***Thenextsizevalueatwhichtoresize.*/privateintthreshold;//Defaultto0这里会有一个面试经常问到的问题:为什么entry数组的大小和初始容量不同一定是2的幂?对于firstKey.threadLocalHashCode&(INITIAL_CAPACITY-1);许多源代码使用hashCode&(-1)而不是hashCode%。这种写法的优点如下:用位运算代替取模,提高计算效率。为了使不同哈希值之间发生碰撞的概率更小,元素在哈希表中尽可能均匀地进行哈希。设置方法publicvoidset(Tvalue){Threadt=Thread.currentThread();ThreadLocalMapmap=getMap(t);if(map!=null)map.set(this,value);elsecreateMap(t,value);}设置方法是还是比较简单的,我们可以关注这个方法中的ThreadLocalMap。既然是map(注意不要和java.util.map混淆,这里指的是概念图),肯定有自己的key和value组成。我们根据源码可以看出,它的关键是可以简单的看做是ThreadLocal,但实际上ThreadLocal存储的是ThreadLocal的一个弱引用,它的值就是我们实际设置的staticclassEntryextendsWeakReference>{/**ThevalueassociatedwiththisThreadLocal.*/Objectvalue;//实际存储的值Entry(ThreadLocalk,Objectv){super(k);value=v;}}Entry是ThreadLocalMap中定义的节点,它继承了WeakReference类,定义了一个Object类型的值,用于存储塞入ThreadLocal的值。我们来看看这个ThreadLocalMap位于哪里?我们看到ThreadLocalMap是一个位于Thread中的变量,我们的值是放在ThreadLocalMap中的,这样就可以实现各个线程之间的隔离。下面两张图基本上把ThreadLocal的结构介绍清楚了。接下来我们看一下ThreadLocalMap中的数据结构。我们知道HaseMap是通过链表和红黑树(jdk1.8)来解决hash冲突的,但是我们看到ThreadLocalMap只有一个数组。它是如何解决hash冲突的呢?ThreadLocalMap采用的是“线性检测”的方式。什么是线性检测?就是根据初始key的hashcode值来确定元素在表数组中的位置。如果发现该位置已经有其他key值的元素被占用,则采用固定算法以一定步长寻找下一个位置,依次判断,直到找到可以入库的位置。ThreadLocalMap解决Hash冲突的方式就是简单的在步长上加1或者减1,找到下一个相邻的位置。/***Incrementimodulolen.*/privatestaticintnextIndex(inti,intlen){return((i+1=0)?i-1:len-1);}这样,如果一个线程中有大量的ThreadLocal,就会出现性能问题,因为每次都需要遍历表,清空是无效值。所以我们在使用的时候尽量少用ThreadLocal,不要在线程中创建大量的ThreadLocal。如果我们需要设置不同的参数类型,我们可以使用ThreadLocal来存储一个Object的Map。这样可以大大减少ThreadLocal的创建数量。.伪代码如下:publicfinalclassHttpContext{privateHttpContext(){}privatestaticfinalThreadLocal>CONTEXT=ThreadLocal.withInitial(()->newConcurrentHashMap(64));publicstaticvoidadd(Stringkey,Tvalue){if(StringUtils.isEmpty(key)||Objects.isNull(value)){thrownewIllegalArgumentException("keyorvalueisnull");}CONTEXT.get().put(key,value);}publicstaticTget(Stringkey){return(T)get().get(key);}publicstaticMapget(){returnCONTEXT.get();}publicstaticvoidremove(){CONTEXT.remove();}}在这种情况下,如果我们需要通过不同的参数,我们可以直接用一个ThreadLocal代替多个ThreadLocal。如果我不想这样玩,我只想创建多个ThreadLocals。这是我的要求,性能一定要好。这可以实施吗?可以使用Netty的FastThreadLocal来解决这个问题,但是需要使用FastThreadLocalThread或者它的子类的thread线程效率会更高,更多的使用方法可以自己查阅资料。接下来我们看一下它的hash函数//生成hashcodegap作为这个幻数,这样生成的值或者ThreadLocalID可以更均匀的分布在一个2的幂大小的数组中。privatestaticfinalintHASH_INCREMENT=0x61c88647;/***Returnsthenexthashcode.*/privatestaticintnextHashCode(){returnextHashCode.getAndAdd(HASH_INCREMENT);}可以看出它在最后构造的ThreadLocalID/threadLocalHashCode0x61c88647上加了一个magicnumber。这个幻数的选择与斐波那契哈希有关。0x61c88647对应的十进制为1640531527,当我们使用幻数0x61c88647累加后,为每个ThreadLocal分配自己的ID,即threadLocalHashCode和2的幂(数组长度)取模,分布得到的结果非常均匀。WecanalsodemonstratethroughthismagicnumberpublicclassMagicHashCode{privatestaticfinalintHASH_INCREMENT=0x61c88647;publicstaticvoidmain(String[]args){hashCode(16);//initialize16hashCode(32);//subsequent2timesexpansionhashCode(64);}privatestaticvoidhashCode(Integerlength){inthashCode=0;for(inti=0;ithreadLocal){newThread(()->{threadLocal.set(newLong[1024*1024*10]);try{Thread.sleep(1000000000);}catch(InterruptedExceptione){e.printStackTrace();}}).start();}通过工具jconsole。jdk自带的exe,你会发现即使执行了gc,内存也不会减少,因为key还是被线程强引用了。效果图如下:对于这种情况,ThreadLocalMap在这种情况的设计中已经考虑到了。只要调用了set()、get()和remove()方法,就会调用cleanSomeSlots()和expungeStaleEntry()方法来清除key为null的值。这是一种被动的清理方法,但是如果不调用ThreadLocal的set()、get()、remove()方法,就会造成value的内存泄漏。它的文档建议我们使用static修饰的ThreadLocal,这样会导致ThreadLocal的生命周期和持有它的类一样长。由于ThreadLocal有强引用,也就是说这个ThreadLocal不会被GC。这种情况下,如果我们不手动删除,Entry的key永远不会为null,弱引用也就失去了意义。所以我们尽量在使用的时候养成一个好习惯,用完之后手动调用remove方法。其实在实际的生产环境中,我们手动移除大部分情况下并不是为了避免key为null的情况,更多的时候是为了保证业务和程序的正确性。比如下订单请求后,我们通过ThreadLocal构造订单的上下文请求信息,然后通过线程池异步更新用户积分。此时如果更新完成,则不执行remove操作,即使下一个新的订单会覆盖原来的值。可能会导致业务问题。如果不想手动清理,有没有其他方法可以解决下面的问题?FastThreadLocal可以去了解一下,它提供了自动回收机制。在线程池场景下,如果程序不停下来,一直重复使用线程,基本不会被销毁。其实本质和上面的例子是一样的。如果线程没有被复用,用完就销毁,不会有泄漏。因为当线程结束时,jvm会主动调用exit方法进行清理。/***ThismethoddiscalledbythesystemtogiveaThread*chancetocleanupbeforeitactuallyexits.*/privatevoidexit(){if(group!=null){group.threadTerminated(this);group=null;}/*积极清空所有引用字段:seebug4006245*/target=null;/*SpeedtheresourceofrelasethreadLocals=null;inheritableThreadLocals=null;inheritedAccessControlContext=null;blocker=null;uncaughtExceptionHandler=null;}InheritableThreadLocal文章开头提到父子之间的变量传递丢失。但是InheritableThreadLocal提供了父子线程之间的数据共享机制。可以解决这个问题。staticThreadLocalthreadLocal=newThreadLocal<>();staticInheritableThreadLocalinheritableThreadLocal=newInheritableThreadLocal<>();publicstaticvoidmain(String[]args)throwsInterruptedException{threadLocal.set("threadLocal主线程值");Thread.sleep(100);newThread(()->System.out.println("子线程获取threadLocal的主线程值:"+threadLocal.get())).start();Thread.sleep(100);inheritableThreadLocal.set("主线程inheritableThreadLocal的值");newThread(()->System.out.println("子线程获取主线程inheritableThreadLocal的值:"+inheritableThreadLocal.get())).start();}输出结果线程获取threadLocal的主线程值:null子线程获取的inheritableThreadLocal的主线程值:inheritableThreadLocal的主线程值但是使用InheritableThreadLocal和线程池时会出现问题,因为子线程只会继承父线程创建线程对象时读取inheritableThreadLocals中的数据复制到自己的inheritableThreadLocals中。这样就实现了父线程和子线程之间的上下文传递。但是在线程池中,线程会被复用,所以就会出现问题。如果要解决这个问题,可以做些什么呢?大家可以考虑一下,或者在下方留言。如果实在不想考虑,可以参考阿里巴巴的transmittable-thread-local。总结简单介绍了ThreadLocal的常用用法,以及大致的实现原理,以及ThreadLocal的内存泄漏问题,以及使用时的注意事项,以及如何解决父子线程之间的传递.介绍了ThreadLocal、InheritableThreadLocal、FastThreadLocal、transmittable-thread-local的各种使用场景和注意事项。本文重点介绍ThreadLocal。如果这一点得到澄清,其他ThreadLocals将得到更好的理解。本文转载自微信公众号“java财经”,可通过以下二维码关注。转载本文请联系爪哇财经公众号。