1前言 此类提供线程本地变量。这些变量与普通变量不同,因为每个访问一个变量(通过其get或set方法)的线程都有其自己的,独立初始化的变量副本。ThreadLocal实例通常是期望将状态与线程(例如,用户ID或事务ID)关联的类中的privatestatic字段。 例如,下面的类生成每个线程本地的唯一标识符。线程的ID是在第一次调用ThreadId。get()时赋值的,并且在以后的调用中保持不变。 只要线程是活跃的并且ThreadLocal实例是可访问的,则每个线程都对其线程本地变量的副本持有隐式的引用。线程消失后,线程本地实例的所有副本都会被GC(除非存在对这些副本的其他引用)。 2继续体系继承?不存在的,这其实也是java。lang包下的工具类图片标题但是ThreadLocal定义带有泛型,说明可以储存任意格式的数据。图片标题3属性ThreadLocal依赖于附加到每个线程(Thread。threadLocals和InheritableThreadLocals)的线程线性探测哈希表。ThreadLocal对象充当键,通过threadLocalHashCode进行搜索。这是一个自定义哈希码(仅在ThreadLocalMaps中有用),它消除了在相同线程使用连续构造的threadlocal的常见情况下的冲突,而在不太常见的情况下仍然表现良好。一句话总结:ThreadLocal通过这样的hashCode,计算当前ThreadLocal在ThreadLocalMap中的索引图片标题图片标题连续生成的哈希码之间的差值,关于该值的设定,可参考文章ThreadLocal的hash算法(关于0x61c88647)图片标题注意static修饰,ThreadLocalMap会被set多个ThreadLocal,而多个ThreadLocal就根据threadLocalHashCode区分图片标题4ThreadLocalMap ThreadLocalMap是自定义的哈希表,仅适用于维护线程本地的值。没有操作导出到ThreadLocal类之外。该类是包私有的,允许在Thread类中的字段声明。为了帮助处理非常长的使用寿命,哈希表节点使用WeakReferences作为键。但是,由于不使用引用队列,因此仅在表空间不足时,才保证删除过时的节点。staticclassThreadLocalMap{此哈希表中的节点使用其主引用字段作为键(始终是一个ThreadLocal对象)继承了WeakReference。请注意,空键(即entry。get()null)意味着不再引用该键,因此可以从表中删除该节点。在下面的代码中,此类节点称为staleentriesstaticclassEntryextendsWeakReferenceThreadL?{与此ThreadLocal关联的值OEntry(ThreadL?k,Objectv){super(k);}}初始容量必须是2的幂privatestaticfinalintINITIALCAPACITY16;table数组,必要时扩容table。length必须是2的幂privateEntry〔〕table中的节点个数privateintsize0;下一次扩容的阈值默认为0特点key是ThreadLocal的引用value是ThreadLocal保存的值数组的数据结构5set5。1ThreadLocalset 将此线程本地变量的当前线程副本设置为指定值。大多数子类将不需要重写此方法,而仅依靠initialValue方法来设置线程本地变量的值。 执行流程获取当前线程获取线程所对应的ThreadLocalMap,从这可以看出每个线程都是独立的,所以此方法天然线程安全判断map是否为null否,则K。V对赋值,k为this,即当前的ThreaLocal对象是,则初始化一个ThreadLocalMap来维护K。V对 来具体看看ThreadLocalMap中的set5。2ThreadLocalMapsetprivatevoidset(ThreadL?key,Objectvalue){新引用指向tableEntry〔〕intlentab。获取对应ThreadLocal在table中的索引,注意这里是hashCode与2幂次长度1(想起来为什么这样计算更好了吗?)intikey。threadLocalHashCode(len1);从该下标开始循环遍历1、如遇相同key,则直接替换value2、如果该key已经被回收失效,则替换该失效的keyfor(Entryetab〔i〕;e!etab〔inextIndex(i,len)〕){ThreadL?ke。get();找到内存地址一样的ThreadLocal,直接替换if(kkey){e。}若k为null,说明ThreadLocal被清理了,则替换当前失效的kif(knull){replaceStaleEntry(key,value,i);}}找到空位,创建节点并插入tab〔i〕newEntry(key,value);table内元素size自增达到阈值(数组大小的三分之二)时,执行扩容if(!cleanSomeSlots(i,sz)szthreshold)rehash();} 注意通过hashCode计算的索引位置i处如果已经有值了,会从i开始,通过1不断的往后寻找,直到找到索引位置为空的地方,把当前ThreadLocal作为key放进去。6getpublicTget(){获取当前线程ThreadtThread。currentThread();获取当前线程对应的ThreadLocalMapThreadLocalMapmapgetMap(t);如果map不为空if(map!null){取得当前ThreadLocal对象对应的EntryThreadLocalMap。Entryemap。getEntry(this);如果不为空,读取当前ThreadLocal中保存的值if(e!null){SuppressWarnings(unchecked)Tresult(T)e。}}否则都执行setInitialValuereturnsetInitialValue();} privateTsetInitialValue(){获取初始值,一般是子类重写TvalueinitialValue();获取当前线程ThreadtThread。currentThread();获取当前线程对应的ThreadLocalMapThreadLocalMapmapgetMap(t);如果map不为nullif(map!null)调用ThreadLocalMap的set方法进行赋值map。set(this,value);否则创建个ThreadLocalMap进行赋值elsecreateMap(t,value);} 接着我们来看下ThreadLocalMapgetEntry得到当前thradLocal对应的值,值的类型是由thradLocal的泛型决定的由于thradLocalMapset时解决数组索引位置冲突的逻辑,导致thradLocalMapget时的逻辑也是对应的首先尝试根据hashcode取模数组大小1索引位置i寻找,找不到的话,自旋把i1,直到找到索引位置不为空为止privateEntrygetEntry(ThreadL?key){计算索引位置:ThreadLocal的hashCode取模数组大小1intikey。threadLocalHashCode(table。length1);Entryetable〔i〕;e不为空,并且e的ThreadLocal的内存地址和key相同,直接返回,否则就是没有找到,继续通过getEntryAfterMiss方法找if(e!nulle。get()key)else这个取数据的逻辑,是因为set时数组索引位置冲突造成的returngetEntryAfterMiss(key,i,e);}自旋i1,直到找到为止privateEntrygetEntryAfterMiss(ThreadL?key,inti,Entrye){Entry〔〕intlentab。在大量使用不同key的ThreadLocal时,其实还蛮耗性能的while(e!null){ThreadL?ke。get();内存地址一样,表示找到了if(kkey)删除没用的keyif(knull)expungeStaleEntry(i);继续使索引位置1elseinextIndex(i,len);etab〔i〕;}}6扩容 ThreadLocalMap中的ThreadLocal的个数超过阈值时,ThreadLocalMap就要开始扩容了,我们一起来看下扩容的逻辑:privatevoidresize(){拿出旧的数组Entry〔〕oldTintoldLenoldTab。新数组的大小为老数组的两倍intnewLenoldLen2;初始化新数组Entry〔〕newTabnewEntry〔newLen〕;intcount0;老数组的值拷贝到新数组上for(intj0;joldLj){EntryeoldTab〔j〕;if(e!null){ThreadL?ke。get();if(knull){e。HelptheGC}else{计算ThreadLocal在新数组中的位置inthk。threadLocalHashCode(newLen1);如果索引h的位置值不为空,往后1,直到找到值为空的索引位置while(newTab〔h〕!null)hnextIndex(h,newLen);给新数组赋值newTab〔h〕e;}}}给新数组初始化下次扩容阈值,为数组长度的三分之二setThreshold(newLen);tablenewT} 源码注解也比较清晰,我们注意两点: 扩容后数组大小是原来数组的两倍;扩容时是绝对没有线程安全问题的,因为ThreadLocalMap是线程的一个属性,一个线程同一时刻只能对ThreadLocalMap进行操作,因为同一个线程执行业务逻辑必然是串行的,那么操作ThreadLocalMap必然也是串行的。7总结 ThreadLocal是非常重要的API,我们在写一个中间件的时候经常会用到,比如说流程引擎中上下文的传递,调用链ID的传递等等,非常好用,但坑也很多。 作者:JavaEdge在掘金 链接:https:juejin。impost5eb423ef6fb9a0435f093e5e