1 ThreadLocal的概述
1.1 ThreadLocal的入门
public class ThreadLocal< T > extends Object
ThreadLocal来自JDK1.2,位于java.lang包。ThreadLocal可以提供线程内的局部变量,这种变量在线程的生命周期内起作用,ThreadLocal又叫做线程本地变量/线程本地存储。
实际上,单就ThreadLocal这个类来说,它不存储任何内容,真正存储数据的集合在每个Thread中的threadLocals变量里面,ThreadLocal中只是定义了这个集合的结构,并提供了一系列操作的方法。后面的源码分析处会讲到!
可以说,ThreadLocal只是一个工具类,一个对各个线程的threadLocals进行操作的工具而已。
ThreadLocal 的作用和目的:
用于实现线程内的数据共享,某些数据是以线程为作用域并且不同线程具有不同的数据副本时,即数据在线程之间隔离,就可以考虑用ThreadLocal。即对于相同的程序代码,多个模块在同一个线程中运行时要共享一份数据,而在另外线程中运行时又共享另外一份数据。
方便同一个线程复杂逻辑下的数据传递,有些时候一个线程中的任务过于复杂,我们又需要某个数据能够贯穿整个线程的执行过程,可能涉及到不同类/函数之间数据的传递。此时使用Threadlocal存放数据,在线程内部只要通过get方法就可以获取到在该线程中存进去的数据,方便快捷。
1.2 同步和ThreadLocal
同步与ThreadLocal是解决多线程中数据访问问题的两种思路,前者是数据共享的思路,后者是数据隔离的思路。同步是一种以时间换空间的思想,ThreadLocal是一种空间换时间的思想。前者仅提供一份变量,让不同的线程排队访问,实现串行化;而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
Threadlocal并不能代替同步,注意ThreadLocal不是用来解决共享对象的多线程访问问题的。通过ThreadLocal的set()方法设置到线程的threadLocals里的是线程自己要存储的对象,其他线程不需要去访问,也是访问不到的。各个线程中的threadLocals以及里面的值都是不同的对象。Threadloocal是用来进行变量隔离,就是说ThreadLocal是针对那些不需要共享的属性!
1.3 主要API方法与使用案例
ThreadLocal类主要有四个可供调用的方法:
void set(T value):保存值;
T get():获取值;
void remove():移除值;
initialValue():返回该线程局部变量的初始值,该方法是为了让子类继承而设计的。这个方法是一个延迟调用方法,在一个线程第一次调用get()时(并且set未被调用)才执行。ThreadLocal中的默认实现是直接返回一个null。
ThreadLocal实现线程内数据共享,线程间数据隔离的案例:
publicclassThreadLocalTest{/***全局ThreadLocal对象位于堆中,这是线程共享的,而方法栈,是每个线程私有的*/staticThreadLocal<String>th=newThreadLocal<>();publicstaticvoidset(){//设置值,值为当前线程的名字th.set(Thread.currentThread().getName());}publicstaticStringget(){//获取值returnth.get();}publicstaticvoidmain(String[]args)throwsInterruptedException{System.out.println("主线程中尝试获取值:"+get());//主线程中设置值,值为线程名字set();//主线程中尝试获取值System.out.println("主线程中再次尝试获取值:"+get());//开启一条子线程Threadthread=newThread(newTh1(),"child");thread.start();//主线程等待子线程执行完毕thread.join();System.out.println("等待子线程执行完毕,主线程中再次尝试获取值:"+get());}staticclassTh1implementsRunnable{@Overridepublicvoidrun(){System.out.println("子线程中尝试获取值:"+get());//子线程中设置值,值为线程名字set();System.out.println("子线程中再次尝试获取值:"+get());}}}
结果如下:
主线程中尝试获取值:null主线程中再次尝试获取值:main子线程中尝试获取值:null子线程中再次尝试获取值:child等待子线程执行完毕,主线程中再次尝试获取值:main
先设置值,然后获取,可以得到“main”。然后开启子线程,在子线程内部,先获取,得到null,然后设置值,再获取,得到“child”。最后在主线程中再尝试获取,得到的还是原值“main”,这说明ThreadLocal使得变量的作用范围限制在本线程中了,其他线程是无法访问到该变量的。
注意这里由于案例演示在最后并没有调用remove方法,在实际使用中应该在使用完毕之后调用remove方法,原理后面会讲!
2 ThreadLocal的原理
2.1 基本关系
ThreadLocal类中定义了一个内部类ThreadLocalMap,ThreadLocalMap是真正存放数据的容器,实际上它的底层就是一张哈希表。
每个Thread线程内部都定义有一个ThreadLocal.ThreadLocalMap
类型的threadLocals
变量,这样,线程之间的ThreadLocalMap互不干扰。threadLocals
变量持有的ThreadLocalMap在ThreadLocal调用set或者get方法时才会初始化。
ThreadLocal还提供相关方法,负责向当前线程的ThreadLocalMap变量获取和设置线程的变量值,相当于一个工具类。
当在某个线程的方法中使用ThreadLocal设置值的时候,就会将该ThreadLocal对象添加到该线程内部的ThreadLocalMap中,其中键就是该ThreadLocal对象,值可以是任意类型任意值。当在某个线程的方法中使用ThreadLocal获取值的时候,会以该ThreadLocal对象为键,在该线程的ThreadLocalMap中获取对应的值。
ThreadLocal中定义了ThreadLocalMap的结构,并提供操作的方法:
publicclassThreadLocal<T>{//……staticclassThreadLocalMap{//……}/***ThreadLocal的构造器,里面什么都没有*创建ThreadLocal时,没有初始化ThreadLocalMap,在set、get方法中还可能初始化!*/publicThreadLocal(){}}
每个thread对象都持有一个ThreadLocalMap类型的引用变量,用于存放线程本地变量。key为ThreadLocal对象,value为要存储的数据。
publicclassThreadimplementsRunnable{/*与此线程相关的线程本地值。此ThreadLocalMap定义在ThreadLocal类中,使用在Thread类中*/ThreadLocal.ThreadLocalMapthreadLocals=null;//………………}
下面是Thread、threadlocalMap、ThreadLocal的关系图:
2.2 基本结构
ThreadLocal中定义了ThreadLocalMap的结构。
ThreadLocalMap也是一张key-value类型的哈希表,但是ThreadLocalMap并没有实现Map接口,它内部具有一个Entry类型的table数组用于存放节点。Entry节点用于存放key、value数据,并且继承了WeakReference。
通过对该ThreadLocal对象进行哈希运算,可以得到该ThreadLocal对象在Entry数组中的桶位,从而找到唯一的Entry。如果发生了哈希冲突,那么与HashMap和Hashtable采用的“链地址法”不同,ThreadLocalMap采用“线性探测法”解决哈希冲突,采用该方法的原因是实现很简单,占用更小的空间,并且一般来说一个ThreadLocalMap并不会存放很多数据!关于哈希表的原理以及各种解决哈希冲突的方法详解,可以看这篇文章:数据结构—散列表(哈希表)的原理以及Java代码的实现。
在创建ThreadLocalMap对象的同时即初始化16个长度的内部table数组,扩容阈值为len * 2 / 3
,扩容增量为增加原容量的1倍
。
在没有使用ThreadLocal设置、获取值时,线程中的ThreadLocalMap对象一直为null。
/***ThreadLocal的内部类ThreadLocalMap*/staticclassThreadLocalMap{/***table数组的初始化容量,*/privatestaticfinalintINITIAL_CAPACITY=16;//存放数据的数组,在创建ThreadLocalMap对象时将会初始化该数组,大小必须是2^N次方privateEntry[]table;//扩容阈值,为len*2/3privateintthreshold;/***内部节点对象,貌似没找到“key”字段在哪里,实际上存放k的属性位于其父类WeakReference的父类Reference中,名为referent,即属性复用*插入数据时,通过对key(threadLocal对象)的hash计算,来找出Entry应该存放的table数组的桶位,*不过可能造成hash冲突,它采用线性探测法解决冲突,因此需要线性向后查找。*/staticclassEntryextendsWeakReference<ThreadLocal<?>>{//存放值Objectvalue;//构造器Entry(ThreadLocal<?>k,Objectv){//调用父类的构造器,传入key,这里k被包装成为弱引用//实际上存放k的属性位于其父类WeakReference的父类Reference中,名为referent,即属性复用super(k);value=v;}}}
2.3 set方法
set方法是由ThreadLocal提供的,用于存放数据,大概步骤如下:
获取当前线程的成员变量threadLocals;
如果threadLocals不等于null,则调用set方法存放数据,方法结束;
否则,调用createMap方法初始化threadLocals,然后存放数据,方法结束。
/***ThreadLocal中的方法,开放给外部调用的存放数据的方法**@paramvalue需要存放的数据*/publicvoidset(Tvalue){//注意,这里首先获取当前线程tThreadt=Thread.currentThread();//1.1然后通过getMap方法,传入t,获取当前t线程的threadLocalsThreadLocalMapmap=getMap(t);//如果map存在,则存放数据if(map!=null)//this代指当前ThreadLocal对象,value表示值map.set(this,value);else//如果不存在,则构建属于当前线程的ThreadLocalMap并存放数据createMap(t,value);}/***1.1ThreadLocal中的方法,获取指定线程的threadLocals**@paramt指定线程*@returnt的threadLocals*/ThreadLocalMapgetMap(Threadt){//t代表当前线程,获取该线程的threadLocals属性,该属性就是一个ThreadLocalMap,默认为nullreturnt.threadLocals;}/***1.2ThreadLocal中的方法,用于构建ThreadLocalMap对象并赋值**@paramt当前线程*@paramfirstValue要存入的值*/voidcreateMap(Threadt,TfirstValue){//该方法是ThreadLocal中的方法,this代指当前ThreadLocal对象t.threadLocals=newThreadLocalMap(this,firstValue);}/***位于ThreadLocalMap中的构造器,用于创建新的ThreadLocalMap对象**@paramfirstKeykey*@paramfirstValuevalue*/ThreadLocalMap(ThreadLocal<?>firstKey,ObjectfirstValue){//创建table数组,初始容量为INITIAL_CAPACITY,即16table=newEntry[INITIAL_CAPACITY];//寻找数组桶位,通过ThreadLocal对象的threadLocalHashCode属性&15inti=firstKey.threadLocalHashCode&(INITIAL_CAPACITY-1);//该位置存放元素,由于是刚创建对象,因此不存在哈希冲突的情况,直接存储就行了//构造器在“基本结构”部分分析过,key最终被包装成弱引用。table[i]=newEntry(firstKey,firstValue);//size设置为1size=1;//setThreshold方法设置扩容阀值setThreshold(INITIAL_CAPACITY);}/***ThreadLocalMap中的方法,设置扩容阈值**@paramlen数组长度*/privatevoidsetThreshold(intlen){//数组长度的2/3threshold=len*2/3;}
2.3.1 内部set方法
上面的set方法中,如果当前t线程的threadLocals不为null,那么又调用了另一个私有的set方法存放数据。该方法是ThreadLocal的核心方法之一,并且比较复杂,大概具有如下步骤:
通过哈希算法计算出当前key存放的桶位i,并获取i的元素e。
如果e不为空,说明发生哈希冲突,使用线性探测法替换或者存放数据:
如果找到了key相等的entry,则它放到无效桶位中,value置为新值,方法结束。
如果没找到key相等的entry,直接在无效slot原地放entry,方法结束。
调用到了replaceStaleEntry方法,那就肯定能将新数据存入ThreadLocalMap中,并且不再执行后续步骤。
如果e的key和指定key相等(使用==比较),那么替换value,方法结束;
否则,如果e的key等于null,那说明是无效数据。调用replaceStaleEntry从该索引开始清理无效数据,并且存放新数据,在replaceStaleEntry过程中:
否则,nextIndex方法获取下一个索引并赋值给i,如果该位置的节点e为null,则结束循环,否则进行下一次循环;
走到这一步,说明没有替换value,也没有没有进行无效数据清理,而是找到了一个空桶位i,直接在该位置插入新entry,此时肯定保证最初始的i和现在的之间的位置都是存在有效节点的;
存放元素完毕之后,再调用cleanSomeSlots做一次部分无效节点清理,如果没清理出去key(返回false)并且当前table大小 大于等于 阈值,则调用rehash方法;
rehash方法中会调用一次全表扫描清理的方法即expungeStaleEntries()方法。如果expungeStaleEntries完毕之后table大小还是大于等于(threshold – threshold / 4),则调用resize方法进行扩容;
resize方法将扩容两倍,同时完成节点的转移。
ThreadLocalMap使用==比较key是否相同。ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1(线性探测),寻找下一个相邻的位置。当向前寻找到数组头部或者向后寻找到数组尾部的时候,下一个位置就是数组尾部或者数组头部,即循环查找。
/***位于ThreadLocalMap内的set方法,用于存放数据。**@paramkeyThreadLocal对象*@paramvalue值*/privatevoidset(ThreadLocal<?>key,Objectvalue){//tab保存数组引用Entry[]tab=table;//len保存数组的度intlen=tab.length;/*1哈希算法计算桶位通过ThreadLocal的threadLocalHashCode属性计算出该key(ThreadLocal对象)对应的数组桶位i*/inti=key.threadLocalHashCode&(len-1);/**2使用线性探测法存放元素,可能进行垃圾清理*获取i索引位置的Entrye,如果e不为null,说明发生了哈希冲突,下面开始解决:*判断两个key是否相等,即是否需要进行value替换,如果相等,则替换value,解决完毕,方法返回;*否则,判断获取的e的key是否为null,如果获取的ThreadLocal为null,这说明该WeakReference(弱引用)被回收了(因为Entry继承了WeakReference),*说明ThreadLocal肯定在外部没有强引用了,这个Entry变成了垃圾,调用replaceStaleEntry方法擦除该位置或者其他的无效的Entry,重新赋值,解决完毕,方法返回,这是为了防止内存泄漏*否则判断该位置i是否为null,即没有节点,如果为null,则在该位置新建节点并插入,解决完毕。*否则,i=nextIndex(i,len),尝试下一次循环。*如果循环完毕,方法还没结束,那说明没找到key相等的节点和key==null的节点,但是找到了下一个节点为null的桶位,记录此时索引值i,将会在该位置插入新节点。**这就是ThreadLocalMap解决哈希冲突的办法,即开放定址法——线性探测:当冲突时,向下查找下一个节点为null的位置存放新节点*nextIndex()方法用于循环数组索引,即如果初始i为15,长度为16,那么nextIndex将返回0,如果初始i为1,长度为16,那么nextIndex将返回2。*这样的做法有利于利用起始索引前面的空间**/for(Entrye=tab[i];e!=null;e=tab[i=nextIndex(i,len)]){/*获取该Entry的key,即原来的ThreadLocal对象,这是其父类Reference的方法*/ThreadLocal<?>k=e.get();/*如果获取的ThreadLocal和要存的ThreadLocal是同一个对象,那么就替换值,方法结束*这里能够看出,判断key相等的条件是两个对象使用==比较返回true**/if(k==key){e.value=value;return;}/**如果获取的ThreadLocal为null,这说明该WeakReference(弱引用)被回收了(因为Entry继承了WeakReference),*说明ThreadLocal肯定在外部没有强引用了,这个Entry变成了垃圾,擦除该位置的Entry,重新赋值并结束方法,这是为了防止内存泄漏*/if(k==null){/**从该位置开始,继续寻找key,并且会尽可能清理其他无效slot*在replaceStaleEntry过程中,如果找到了key,则做一个swap把它放到那个无效slot中,value置为新值*在replaceStaleEntry过程中,没有找到key,直接在该无效slot原地放entry**/replaceStaleEntry(key,value,i);return;}}/**执行到这一步方法还没有返回,说明i位置没有节点,此时e等于null,直接在该位置插入新的Entry*此时肯定保证最初始的i和现在的之间的位置是存在节点的!**/tab[i]=newEntry(key,value);//size自增1,使用sz记录intsz=++size;/*3尝试清理垃圾,然后判断是否需要扩容,如果需要那就扩容*存放完毕元素之后,再调用cleanSomeSlots做一次垃圾清理,如果没清理出去key(返回false)*并且当前table大小大于等于阈值,则调用rehash方法*rehash方法中会调用一次全量清理slot方法也即expungeStaleEntries()方法*如果expungeStaleEntries完毕之后table大小还是大于等于(threshold–threshold/4),则调用resize方法进行扩容*resize方法将扩容两倍,同时完成节点的转移**/if(!cleanSomeSlots(i,sz)&&sz>=threshold)rehash();}/***在length的索引范围内获取i的下一个索引,循环*/privatestaticintnextIndex(inti,intlen){return((i+1<len)?i+1:0);}/***在length的索引范围内获取i的上一个索引,循环*/privatestaticintprevIndex(inti,intlen){return((i-1>=0)?i-1:len-1);}privatevoidrehash(){expungeStaleEntries();//Uselowerthresholdfordoublingtoavoidhysteresisif(size>=threshold-threshold/4)resize();}
2.3.2 哈希算法
在set方法中,我们可以找到ThreadLocalMap的哈希算法为:
int i = key.threadLocalHashCode & (len - 1);
由于len长度一定是2的幂次方,因此上面的位运算可以转换为key.threadLocalHashCode% len
,所以说ThreadLocalMap的哈希算法也是一种取模(求余)算法
,因为余数一定会比除数小,那么计算出来的桶位肯定是位于[0, len-1]之间了,刚好在底层数组的索引范围内,还是比较简单的。
这里的key我们知道是ThreadLocal对象,这个threadLocalHashCode
属性看名字猜测就是该对象的哈希值了,那么这个值是通过hashCode方法得到的吗?实际上,threadLocalHashCode
这个属性的得来非常的有趣,我们必须要去ThreadLocal源码中去看看!
publicclassThreadLocal<T>{/***下一个hashCode*注意:这是个静态属性,那么只有在ThreadLocal的类第一次被加载进行类初始化的时候会被初始化,明显,初始化时为0。*/privatestaticAtomicIntegernextHashCode=newAtomicInteger();/***threadlocal对象的hashcode,并非通过HashCode方法得到,他有自己的计算规则*可以看到,它是调用nextHashCode()方法的返回值得来的*/privatefinalintthreadLocalHashCode=nextHashCode();/***每个threadLocal对象通过该方法获取自己的hashcode*/privatestaticintnextHashCode(){//内部使用nextHashCode对象的getAndAdd方法//该方法首先返回当前的值,然后使得当前值的值加上指定的值,这里是HASH_INCREMENTreturnnextHashCode.getAndAdd(HASH_INCREMENT);}/***哈希增量,顾名思义,就是哈希值的增量*/privatestaticfinalintHASH_INCREMENT=0x61c88647;}
结合上面的几个属性和方法,我们终于明白:
在第一次创建ThreadLocal实例时,会加载ThreadLocal类,此时nextHashCode初始化值为0,然后是该对象threadLocalHashCode属性的初始化,在创建该类对象完毕之后,会自动调用nextHashCode方法,将此时nextHashCode的值作为自己的hashCode并且nextHashCode对象的值增加HASH_INCREMENT,明显是作为下一个ThreadLocal实例的hashCode值。
即,每一个ThreadLocal实例使用创建该实例时的nextHashCode值作为自己的hashCode,然后将nextHashCode值增加HASH_INCREMENT,作为下一个ThreadLocal实例的hashCode。
0x61c88647
是十六进制的数,转换为十进制就是1640531527,实际上这个哈希增量的值的选取和斐波那契散列法、黄金比例有关(https://www.javaspecialists.eu/archive/Issue164.html),目的是为了让哈希码能更加均匀的分布在2的N次方的数组里。
2.4 get方法
对于不同的线程,每次获取变量值时,是从本线程内部的threadLocals中获取的,别的线程并不能获取到当前线程的值,形成了变量的隔离,互不干扰。大概步骤如下:
获取当前线程的成员变量threadLocals
如果threadLocals非空,调用getEntry方法尝试查找并返回节点e:
如果e不为null,说明找到了,那爱么返回e的value,方法结束
如果e为null,说明没找到,方法继续。
到这一步,说明可能是threadLocals为空,或者没找到e。那么调用setInitialValue方法,以当前ThreadLocal对象为key设置一个entry,并返回value。
/***ThreadLocal中的get方法,开放给外部调用**@return当前线程的当前ThreadLocal对象存入的值*/publicTget(){//获取当前线程Threadt=Thread.currentThread();//获取当前线程的threadLocals对象ThreadLocalMapmap=getMap(t);//如果map不为null,即表示已经初始化过if(map!=null){//从map获取对应的Entry节点,传入this代表当前的ThreadLocal对象ThreadLocalMap.Entrye=map.getEntry(this);//如果e不为nullif(e!=null){//获取并返回值Tresult=(T)e.value;returnresult;}}//否则,如果map为null,或者e为null//那么返回null或者自定义的初始值returnsetInitialValue();}
2.4.1 getEntry方法
ThreadLocalMap内部的方法,根据key,尝试获取对应的Entry节点。 大概步骤如下:
根据key计算出桶位;
获取该桶位节点e,如果e不为null并且e的key和指定key相等(使用==比较),那么返回e,方法结束;
否则,调用getEntryAfterMiss方法进行一个步长的线性探测查找,查找过程中每碰到无效的节点,调用expungeStaleEntry进行清理;如果找到了则返回找到的entry;没有找到(探测到了空的桶位),则返回null。
/***ThreadLocalMap内部的方法,根据key,获取对应的Entry节点**@paramkeykey*@returnEntry节点,没找到就返回null*/privateEntrygetEntry(ThreadLocal<?>key){//根据key计算桶位inti=key.threadLocalHashCode&(table.length-1);//获取Entry节点eEntrye=table[i];/*如果e不为nul并且并且e内部key等于当前key(ThreadLocal对象)*///可以看到key相等是使用==直接比较的if(e!=null&&e.get()==key)//则返回ereturne;else/*否则使用线性探测查找线性探测查找过程中每碰到无效slot,调用expungeStaleEntry进行清理;如果找到了则返回entry;没有找到,返回null*/returngetEntryAfterMiss(key,i,e);}
2.4.2 setInitialValue方法
ThreadLocal的方法,用于设置并返回初始值,在get方法没有找key对应的节点时,会调用该方法!
大概有如下几步:
获取initialValue方法的返回值,作为新节点的value;
获取当前线程的ThreadLocalMap,判断是否为null;
如果不为null,则以当前ThreadLocal对象为key,存放value,方法结束;
如果为null,则初始化此线程的ThreadLocalMap,并以当前ThreadLocal对象为key,存放value,方法结束。
主线程中尝试获取值:null主线程中再次尝试获取值:main子线程中尝试获取值:null子线程中再次尝试获取值:child等待子线程执行完毕,主线程中再次尝试获取值:main0
2.4.2.1 initialValue方法
当get方法没有找到数据时,会调用setInitialValue方法,该方法中会调用initialValue方法,将默认返回null,用户也可以重写该方法,用于返回指定的值,相当于默认初始值。
setInitialValue将会以当前ThreadLocal对象为key,initialValue的返回值为value,存放一个节点,同时返回value的值。
主线程中尝试获取值:null主线程中再次尝试获取值:main子线程中尝试获取值:null子线程中再次尝试获取值:child等待子线程执行完毕,主线程中再次尝试获取值:main1
2.4.2.2 默认初始值案例
主线程中尝试获取值:null主线程中再次尝试获取值:main子线程中尝试获取值:null子线程中再次尝试获取值:child等待子线程执行完毕,主线程中再次尝试获取值:main2
2.5 ThreadLocal的内存泄露
2.5.1 内存泄漏的原理
首先是基础知识,关于Java中的引用的介绍:Java中强、软、弱、虚四种对象引用的详解和案例演示。
根据上面的源码,我们知道在存放新结点时在Entry结点的构造器中,并不是直接使用ThreadLocal对象作为key的,而是使用由ThreadLocal对象包装成的弱引用对象作为Key的,key被弱引用的字段关联,获取key是也是从弱引用字段中获取的。
为什么使用弱引用包装的ThreadLocal对象作为key? 因为如果某个entry直接使使用一个普通属性和ThreadLocal对象关联,即key是强引用。那么当最外面ThreadLocal对象的全局变量引用置空时,由于在ThreadLocalMap中存在key对这个ThreadLocal对象的强引用,那么这个ThreadLocal对象并不会被回收,但此时我们已经无法访问、利用这个对象,造成了key的内存泄漏。
因此,ThreadLocal对象被包装为弱引用作为key。这样,当外部的ThreadLocal对象的强引用被清除时,由于在ThreadLocalMap中存储的是弱引用key,这个ThreadLocal对象只被弱引用对相关联,因此它就是一个弱引用对象,那么下一次GC时这个弱引用ThreadLocal对象可以自动被清除了。
但是,此时仍然会造成内存泄漏,不过此时是value或者说Entry的内存泄漏。
我们知道value是强引用。这就导致了一个问题,如果这个弱引用key被回收而变成null时,如果之前调用ThreadLocal方法设置值的线程一直持续运行,那么它的ThreadLocalMap也一直存在,那么内部的entry结点也一直存在,那么value肯定还存在,但是此时却不能通过key访问到了(因为key被回收变成null了),此时还是发生了内存泄露。
所以说,最保险的办法是移除无效的Entry。
2.5.2 如何避免内存泄漏
我们在set和get方法的源码中能够看到,当遍历的entry的key为null时,此时将清除该entry,value置空,这样就可以解决部分内存泄漏问题。但这并不是绝对的,可能并没有遍历到key为null的entry时set、get方法就因为插入、获取成功而返回了,因此在set、get方法中,只会尝试将遍历的到无效数据清除,并且这种方式是一种被动的清除,不能即时清除无效数据。
ThreadLocal还有一个remove方法,该方法可以将此ThreadLocal对象对应的entry清除。实际上,在对ThreadLocal的数据使用完毕之后,从逻辑上来说此时的entry就是无效的数据了,因此主动调用一次remove方法,将该entry移除。这样我们对使用完毕的entry进行手动清除,从根本上杜绝了内存泄漏问题。
所以养成良好的编程习惯十分重要,使用完ThreadLocal的数据之后,一定要记得调用一次remove方法。
3 总结与应用
总结:
每个ThreadLocal由于实现线程本地存储,但是只能保存一个本地数据,如果想要一个线程能够保存多个数据,就需要创建多个ThreadLocal。
ThreadLocalMap的key键为ThreadLocal包装成的弱引用,value为设置的值。ThreadLocal会有内存泄漏的风险,因此使用完毕必须手动调用remove清除!
应用:
使用ThreadLocal的典型场景正如上面的数据库连接管理,线程会话管理等场景,只适用于独立变量副本的情况,如果变量为全局共享的,则不适用在高并发下使用。
Spring MVC对于每一个请求线程的Request对象使用ThreadLocal属性封装到RequestContextHolder中,这样每条线程都能访问到自己的Request。
Spring 声明式事务管理中,每一个事务的信息也是使用ThreadLocal属性封装到TransactionSynchronizationManager中的,以此实现不同线程的事务存储和隔离,以及事务的挂起和恢复。
原始的JDBC方式的时候可以使用ThreadLocal类来管理事务!