当前位置: 首页 > 后端技术 > Node.js

12.ThreadLocal的小秘密

时间:2023-04-03 22:53:36 Node.js

好久不见了。不知道大家过年过得怎么样?你有没有感到轻松?还能领到很多压岁钱吗?好了,废话不多说,开始今天的话题:ThreadLocal。我在面试中收集了4个关于ThreadLocal的常见问题:什么是ThreadLocal?ThreadLocal在什么场景下使用?ThreadLocal底层是如何实现的?ThreadLocal在什么情况下会发生内存泄漏呢?使用ThreadLocal需要注意什么?我们先从一个“谣言”说起,分析ThreadLocal的源码,尝试纠正“谣言”引起的误解,回答上述问题。流传已久的“谣言”中很多文章都说“ThreadLocal通过复制共享变量解决并发安全问题”,例如:这种说法并不准确,容易被误解为ThreadLocal会复制shared变量。让我们看一个例子:privatestaticfinalDateFormatDATE_FORMAT=newSimpleDateFormat("yyyy-MM-dd");publicstaticvoidmain(String[]args)throwsInterruptedException{for(inti=0;i<1000;i++){newThread(()->{try{System.out.println(DATE_FORMAT.parse("2023-01-29"));}catch(ParseExceptione){e.printStackTrace();}}).start();}}复制代码我们知道,多个线程并发访问同一个DateFormat实例对象会造成严重的并发安全问题问题,那么加入ThreadLocal是否可以解决并发安全问题呢?修改代码:/**第一种写法*/privatestaticfinalThreadLocalDATE_FORMAT_THREAD_LOCAL=newThreadLocal<>(){@OverrideprotectedDateFormatinitialValue(){returnDATE_FORMAT;}};publicstaticvoidmain(String[]args)throwsInterruptedException{for(inti=0;i<1000;i++){newThread(()->{try{System.out.println(DATE_FORMAT_THREAD_LOCAL.get().parse("2023-01-29"));}catch(ParseExceptione){e.printStackTrace();}}).start();}}复制代码,估计很多朋友会说:“你写错了!《阿里巴巴Java开发手册》不是这样用的!”把书中的用法搬过来:/**第二种写法*/privatestaticfinalThreadLocalDATE_FORMAT_THREAD_LOCAL=newThreadLocal<>(){@OverrideprotectedDateFormatinitialValue(){returnnewSimpleDateFormat("yyyy-MM-dd");}};CopyCodeTips:代码略有改动~~看看两种写法的区别:第一种写法在使用ThreadLocal#initialValue时使用了共享变量DATE_FORMAT;第二种写法在使用ThreadLocal#initialValue时创建一个SimpleDateFormat对象。根据“传闻”的描述,第一种写法会复制DATE_FORMAT的副本,供不同线程使用,但从结果来看,ThreadLocal并没有这样做。有的朋友可能会怀疑是DATE_FORMAT_THREAD_LOCAL线程共享导致的,但是别忘了第二种写法也是线程共享。至此,我们应该可以猜到,在第二种写法中,每个线程都会访问不同的SimpleDateFormat实例对象,接下来我们通过源码一探究竟。ThreadLocal的实现除了使用ThreadLocal#initialValue之外,还可以通过ThreadLocal#set添加变量后使用:ThreadLocalthreadLocal=newThreadLocal<>();threadLocal.set(newSimpleDateFormat("yyyy-MM-dd"));System.out.println(threadLocal.get().parse("2023-01-29"));拷贝代码3步就可以完成:创建对象,添加变量,取出变量,无参构造函数(空实现)没什么好说的,我们从ThreadLocal#set开始。ThreadLocal#set的实现ThreadLocal#set的源码:publicvoidset(Tvalue){,Threadt=Thread.currentThread();//获取当前线程的ThreadLocalMapmap=getMap(t);if(map!=null){//添加变量map.set(this,value);}else{//初始化ThreadLocalMapcreateMap(t,value);}}复制代码ThreadLocal#set的源码很简单,但是揭示了一个许多重要信息:变量存储在ThreadLocalMap中,与当前线程相关;ThreadLocalMap的实现应该和Map类似。然后看源码:}publicclassThreadimplementsRunnable{ThreadLocal.ThreadLocalMapthreadLocals=null;}复制的代码清楚地表明了ThreadLocalMap和Thread的关系:ThreadLocalMap是Thread的一个成员变量,每个Thread实例对象都有自己的ThreadLocalMap。另外,你还记得线程必须知道的8个问题(上文)中提到的Thread实例对象和执行线程的关系吗?从Java的角度来看,可以认为创建Thread类的实例对象就完成了线程的创建,调用Thread.start0可以认为是操作系统层面线程的创建和启动。可以近似为:线程实例对象≈执行线程线程实例对象\近似执行线程线程实例对象≈执行线程。也就是说,属于Thread实例对象的ThreadLocalMap也属于每一个执行线程。基于以上,我们似乎得到了一个特殊的变量作用域:属于线程。Tips:其实它属于线程,也就是属于Thread实例对象,因为Thread是Java中thread的抽象;ThreadLocalMap属于线程,但不代表ThreadLocalMap中存储的变量属于线程。ThreadLocalMap的实现ThreadLocalMap是ThreadLocal的一个内部类,代码并不复杂:publicclassThreadLocal{privatefinalintthreadLocalHashCode=nextHashCode();staticclassThreadLocalMap{staticclassEntryextendsWeakReference>{对象值;条目(ThreadLocal<?>k,对象v){超级(k);值=v;}}私人条目[]表;私有整数大小=0;私有整数阈值;privatevoidsetThreshold(intlen){threshold=len*2/3;}ThreadLocalMap(ThreadLocalfirstKey,ObjectfirstValue){table=newEntry[INITIAL_CAPACITY];inti=firstKey.threadLocalHashCode&(INITIAL_CAPACITY-1);table[i]=newEntry(firstKey,firstValue);大小=1;设置阈值(初始容量);}}}复制代码仅从结构和构造方法,已经可以看出ThreadLocalMap的特点:ThreadLocalMap的底层存储结构是Entry数组;使用ThreadLocal的hash值取模,定位数组标记;构造器使用该方法添加变量时,会存储原始变量。很明显,ThreadLocalMap是一个哈希表的实现,ThreadLocal作为Key。我们可以把ThreadLocalMap看成是一个“简化”的HashMap。Tips:本文不讨论哈希表实现中处理哈希冲突和数组扩展的方法;不需要关注ThreadLocalMap#set和ThreadLocalMap#getgetEntry的实现;和构造方法一样,ThreadLocalMap#set存储的是原始变量。至此,无论是ThreadLocalMap#set,还是ThreadLocalMap的构造方法,都是存储原始变量,没有进行任何复制操作。也就是说,如果你想通过ThreadLocal来隔离线程之间的变量,你需要为每个线程手动创建自己的变量。ThreadLocal#get的实现ThreadLocal#get的源码也很简单:publicTget(){Threadt=Thread.currentThread();ThreadLocalMapmap=getMap(t);if(map!=null){ThreadLocalMap.条目e=map.getEntry(this);if(e!=null){@SuppressWarnings("unchecked")T结果=(T)e.value;返回结果;}}returnsetInitialValue();}复制前面的部分代码很容易理解,我们看一下当map==null时调用的ThreadLocal#setInitialValue方法:privateTsetInitialValue(){Tvalue=initialValue();Threadt=Thread.currentThread();ThreadLocalMapmap=getMap(t);if(map!=null){map.set(this,value);}else{createMap(t,value);}if(thisinstanceofTerminatingThreadLocal){TerminatingThreadLocal.register((TerminatingThreadLocal)this);}returnvalue;}代码ThreadLocal#setInitialValue方法和ThreadLocal#set几乎一样,只是通过ThreadLocal#initialValue获取变量。如果变量是通过ThreadLocal#initialValue添加的,那么在第一次调用ThreadLocal#get时,变量就存储在ThreadLocalMap中。ThreadLocal的原理说的很好,到这里我们已经可以对ThreadLocal建立一个比较完整的认识。我们先看一下ThreadLocal、ThreadLocalMap和Thread的关系:可以看到ThreadLocal是作为ThreadLocalMap中的Key,而ThreadLocalMap是Thread中的一个成员变量,属于每个Thread实例对象。忘了ThreadLocalMap是ThreadLocal的内部类吧,整体结构会很清晰。在创建ThreadLocal对象并存储数据时,会为每个Thread对象创建一个ThreadLocalMap对象并存储数据,并以ThreadLocal对象作为Key。在每个Thread对象的生命周期中,可以通过ThreadLocal对象访问存储的数据。