什么是ThreadLocal
ThreadLocal
即:线程的局部变量,主要是存放每个线程的私有数据。 每当你创建一个ThreadLocal
变量,那么访问这个变量的每个线程都会在当前线程存一份这个变量的本地副本,只有自身线程能够访问,和其他线程是不共享的,这样可以避免线程资源共享变量冲突的问题。
ThreadLocal的基本使用方式
publicclassThreadLocalDemoOne{/***创建ThreadLocal变量*/privatestaticThreadLocal<Integer>intLocal=newThreadLocal<>();/***创建ThreadLocal并初始化赋值*/privatestaticThreadLocal<Integer>intLocal2=ThreadLocal.withInitial(()->6);publicstaticvoidmain(String[]args){//设置变量值intLocal.set(8);//读取变量值System.out.println("intLocaldata:"+intLocal.get());//清空变量值intLocal.remove();System.out.println("intLocaldata:"+intLocal.get());System.out.println("intLocaldata:"+intLocal2.get());}}
ThreadLocal 基本的数据结构
从
Thread
类源代码入手,可以看到Thread
中 存有两个ThreadLocal.ThreadLocalMap
的对象
publicclassThreadimplementsRunnable{//......//与此线程有关的ThreadLocal值。由ThreadLocal类维护ThreadLocal.ThreadLocalMapthreadLocals=null;//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护ThreadLocal.ThreadLocalMapinheritableThreadLocals=null;//......}
Thread
类中的变量存储变量私有的 ThreadLocal值
threadLocals: 线程私有的ThreadLocal 的值
inheritableThreadLocals:可以被线程继承的 线程私有的值
ThreadLocalMap
ThreadLocalMap
是 ThreadLocal 类实现的定制化的 HashMap
staticclassThreadLocalMap{staticclassEntryextendsWeakReference<ThreadLocal<?>>{/**ThevalueassociatedwiththisThreadLocal.*/Objectvalue;Entry(ThreadLocal<?>k,Objectv){super(k);value=v;}}/***初始化空间大小*/privatestaticfinalintINITIAL_CAPACITY=16;/***Thetable,resizedasnecessary.*table.lengthMUSTalwaysbeapoweroftwo.*/privateEntry[]table;
key:就是当前线程的 就是 ThreadLocal 对象
value: 通过 set 设置的值
注意:
Entry extends WeakReference<ThreadLocal<?>>
table 中的 key 是一个 弱引用,这是个值得探讨的点、Java 为何要设计 key 为 弱引用呢?
总结:
最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。
InheritableThreadLocal
InheritableThreadLocal
主要用于将 主线程的ThreadLocal
对象, 传递到子线程中
publicclassInheritableThreadLocalDemo{privatestaticThreadLocal<Integer>intLocal=newThreadLocal<>();privatestaticInheritableThreadLocal<Integer>intInheritableLocal=newInheritableThreadLocal<>();publicstaticvoidmain(String[]args){intLocal.set(1);intInheritableLocal.set(2);Threadthread=newThread(()->{System.out.println(Thread.currentThread().getName()+“:”+intLocal.get());System.out.println(Thread.currentThread().getName()+“:”+intInheritableLocal.get());});thread.start();}}
执行结果
Thread-0:nullThread-0:2
可以看到 声明 InheritableThreadLocal
的对象,是能被子线程继承到的。
ThreadLocal 内存泄露问题
内存泄露
程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光
我看到的很多文章都说,内存泄露 是由于 ThreadLocalMap
的 key
为弱引用导致的,弱引用对象,在没有被外部引用时,当发生GC 是 key 被回收为 null, 但是 value 还存在强引用,可能会存在 内存泄露问题
但其实,由于 Thread -> ThreadLocalMap -> Entry -> value 存在这样一条引用链 只要 Thread
不被退出,ThreadLocalMap
的生命周期将是一样长的,如果不进行手动删除,必然会出现内存泄露。更何况我们大多数是以线程池的方式去操作线程。
那又是如果解决的内存泄露 ?
ThreadLocal
设置了两层保障:
key : 创建为弱引用对象
调用 set()
, get()
, remove()
都会对 key = null 进行清除 value 操作
总结:
threadLocal
内存泄漏的根源是:由于 ThreadLocalMap
的生命周期跟 Thread
一样长,如果没有手动删除对应 key
就会导致内存泄漏,而不是因为弱引用。
建议:在使用ThreadLocal的时候要养成及时 remove()
的习惯
源码中分析防止内存泄露的 清除操作
ThreadLocal
中有两种清除方式:
expungeStaleEntry() 探测式清理
cleanSomeSlots() 启发式清除
remove() 源码
publicvoidremove(){//获取当前线程绑定的threadLocalsThreadLocalMapm=getMap(Thread.currentThread());//当map不为null进行移除当前线程中指点的ThreadLocal对象的值if(m!=null)m.remove(this);}privatevoidremove(ThreadLocal<?>key){Entry[]tab=table;intlen=tab.length;//计算ThreadLocalkey的下标值inti=key.threadLocalHashCode&(len-1);//循环遍历只要不为nullfor(Entrye=tab[i];e!=null;e=tab[i=nextIndex(i,len)]){//比对key值,如果相等的话if(e.get()==key){//调用clear()方法清理掉e.clear();//执行探测式清理将key为null的节点进行清除expungeStaleEntry(i);return;}}}privateintexpungeStaleEntry(intstaleSlot){Entry[]tab=table;intlen=tab.length;//expungeentryatstaleSlottab[staleSlot].value=null;tab[staleSlot]=null;size--;//RehashuntilweencounternullEntrye;inti;for(i=nextIndex(staleSlot,len);(e=tab[i])!=null;i=nextIndex(i,len)){ThreadLocal<?>k=e.get();//对table中key为null进行处理,将value设置为null,清除value的引用if(k==null){e.value=null;tab[i]=null;size--;}else{inth=k.threadLocalHashCode&(len-1);if(h!=i){tab[i]=null;//这里主要的作用是由于采用了开放地址法,所以删除的元素是多个冲突元素中的一个,需要对后面的元素作//处理,可以简单理解就是让后面的元素往前面移动//UnlikeKnuth6.4AlgorithmR,wemustscanuntil//nullbecausemultipleentriescouldhavebeenstale.while(tab[h]!=null)h=nextIndex(h,len);tab[h]=e;}}}returni;}
get() 源码
publicTget(){//获取当前线程绑定的threadLocalsThreadt=Thread.currentThread();ThreadLocalMapmap=getMap(t);//当map不为nullif(map!=null){//查询当前ThreadLocal变量实例对应的Entry(获取中内部调用了清除)ThreadLocalMap.Entrye=map.getEntry(this);//如果不为null,获取value,返回if(e!=null){@SuppressWarnings("unchecked")Tresult=(T)e.value;returnresult;}}//当map为null进行初始化returnsetInitialValue();}privateEntrygetEntry(ThreadLocal<?>key){inti=key.threadLocalHashCode&(table.length-1);Entrye=table[i];//对应的entry存在且ThreadLocal就是key,则命中返回if(e!=null&&e.get()==key)returne;else//如果不是则进行线性探针,往后进行查找元素returngetEntryAfterMiss(key,i,e);}privateEntrygetEntryAfterMiss(ThreadLocal<?>key,inti,Entrye){Entry[]tab=table;intlen=tab.length;while(e!=null){ThreadLocal<?>k=e.get();//该entry当前的ThreadLocal返回数据if(k==key)returne;//该entry对应的ThreadLocal已经被回收进行探测式清除if(k==null)expungeStaleEntry(i);else//指向下个槽位往下循环i=nextIndex(i,len);e=tab[i];}returnnull;}
set() 源码
privatevoidset(ThreadLocal<?>key,Objectvalue){//....省略部分代码for(Entrye=tab[i];//....省略部分代码//冲突位key为null则说明被GC回收,进行清理回收数据if(k==null){//清理key被GC为null的数据replaceStaleEntry内部进行探针式清除replaceStaleEntry(key,value,i);return;}}//....省略部分代码//检查是否需要进行扩容cleanSomeSlots(i,sz)启发式清除if(!cleanSomeSlots(i,sz)&&sz>=threshold)rehash();}
通过
set()
,get()
,remove()
三个方法的源码调用查看,可以明确的知道ThreadLocal
做了很多清除操作,为了防止 内存泄露
具体的清除流程
expungeStaleEntry: 是对 ThreadLocal 被回收的节点开始,向后进行查找时候还存在被回收的节点进行清除操作
/***探测式清除*@paramstaleSlot为null的节点位置*@return*/privateintexpungeStaleEntry(intstaleSlot){Entry[]tab=table;intlen=tab.length;//expungeentryatstaleSlottab[staleSlot].value=null;tab[staleSlot]=null;size--;//RehashuntilweencounternullEntrye;inti;for(i=nextIndex(staleSlot,len);(e=tab[i])!=null;i=nextIndex(i,len)){ThreadLocal<?>k=e.get();//对table中key为null进行处理,将value设置为null,清除value的引用if(k==null){e.value=null;tab[i]=null;size--;}else{inth=k.threadLocalHashCode&(len-1);if(h!=i){tab[i]=null;//这里主要的作用是由于采用了开放地址法,所以删除的元素是多个冲突元素中的一个,需要对后面的元素作//处理,可以简单理解就是让后面的元素往前面移动//UnlikeKnuth6.4AlgorithmR,wemustscanuntil//nullbecausemultipleentriescouldhavebeenstale.while(tab[h]!=null)h=nextIndex(h,len);tab[h]=e;}}}returni;}
cleanSomeSlots: 是从指定节点位置,通过n 来控制查找的次数,进行多次清除操作
expungeStaleEntry 返回了下一个为空的节点位置
cleanSomeSlots 会从 下一个为空节点位置,再次进行扫描操作
具体能进行几次扫描 ,第一次是 传递来的 n , 从第二次开始 是 table 的 长度来决定的
/***启发式清除*@parami当前的节点位置*@paramn是用于控制控制扫描次数的*@return*/privatebooleancleanSomeSlots(inti,intn){booleanremoved=false;Entry[]tab=table;intlen=tab.length;do{i=nextIndex(i,len);Entrye=tab[i];if(e!=null&&e.get()==null){//扩大扫描控制因子设置成表的长度n=len;removed=true;//使用探测式清除进行清除操作i=expungeStaleEntry(i);}}while((n>>>=1)!=0);returnremoved;}
总结:
可以看到 cleanSomeSlots
底层 还是通过 expungeStaleEntry
去进行清除的。但是 cleanSomeSlots
清除范围 要比 expungeStaleEntry
大
ThreadLocalMap 的Hash冲突
ThreadLocal
底层 没有java.util.HashMap
作为底层的Map 数据结构 所以需要使用不同的 Hash冲突解决方案java.util.HashMap
使用 数组 + 链表 (链表一定长度转化为红黑树)的方式解决 Hash 冲突问题ThreadLocalMap
则是使用 线性探测法
publicclassThreadimplementsRunnable{//......//与此线程有关的ThreadLocal值。由ThreadLocal类维护ThreadLocal.ThreadLocalMapthreadLocals=null;//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护ThreadLocal.ThreadLocalMapinheritableThreadLocals=null;//......}0
ThreadLocalMap 的扩容机制
相关扩容参数:
publicclassThreadimplementsRunnable{//......//与此线程有关的ThreadLocal值。由ThreadLocal类维护ThreadLocal.ThreadLocalMapthreadLocals=null;//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护ThreadLocal.ThreadLocalMapinheritableThreadLocals=null;//......}1
具体的 set()
方法
publicclassThreadimplementsRunnable{//......//与此线程有关的ThreadLocal值。由ThreadLocal类维护ThreadLocal.ThreadLocalMapthreadLocals=null;//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护ThreadLocal.ThreadLocalMapinheritableThreadLocals=null;//......}2
从基本的参数 、扩容 参数 、以及 set() 中的 扩容判断 我们知道 基本的扩容判断
启发式清除操作 没有清除到数据时
当前 表中元素个数 >= threshold (16 * 2/ 3 = 10.6)
但真的是这样吗?
接着查看 rehash()
方法
publicclassThreadimplementsRunnable{//......//与此线程有关的ThreadLocal值。由ThreadLocal类维护ThreadLocal.ThreadLocalMapthreadLocals=null;//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护ThreadLocal.ThreadLocalMapinheritableThreadLocals=null;//......}3
总结:
ThreadLocalMap
的扩容操作,进行了2次 Null
key 元素的清除,并在每次清除后进行 阀值计算。个人理解这样的操作的原因,主要还是由于扩容时,需要进行元素位置的移动操作,为了减少移动操作。 ::注:: 元素位移时 会再一次 对 Null
key 元素的进行清除操作
【相关资料】
ThreadLocal的内存泄露?什么原因?如何避免?