学习笔记——ThreadLocal
简介:
ThreadLocal :线程本地变量、线程私有。
实现原理
其实现原理是在Thread类中引用一个ThreadLocalMap结构的成员变量。
ThreadLocalMap是一个Entry类型的数组结构,与HashMap不同的是当出现hash冲突时,ThreadLocalMap并不是以链表的方式组织数据的,而是依次向数组后方查找空白位置进行保存(后来才知道,这种解决冲突的方法叫做线性探测法)
Entry是一个以ThreadLocal弱引用为key的k-v结构。
需要注意的点(个人理解)
-
线程复用造成内存泄漏
内存泄漏:应用程序内不使用的对象无法被GC进行回收。
内存溢出:应用程序无法申请到所需要的内存空间。(例如OOM)
当线程为线程池的核心线程时,Thread对象由于需要被复用将不会被JVM进行回收,因此Thread内部的成员变量threadlocals也不会被回收。threadlocals是ThreadLocalMap类型的,ThreadLocalMap中存在一个以K-V类型为节点的entry数组。entry中的Key为ThreadlLocal的弱引用,threadlocal对应Value的引用是强引用。因此线程复用后value无法被GC回收 -
Entry中ThreadLocal为什么要被设计为弱引用
一般来讲当我们在代码中将ThreadLocal的强引用设置为null后,Threadlocal就理应被GC回收了,但实际上却并非如此。
假如线程被线程池复用,那么此时Thread中的threadlocals中的Entry还包含着ThreadLocal的引用。所以ThreadLocal无法被被GC回收导致内存泄漏(key内存泄漏), 因此将Entry中的ThreadLocal设计为弱引用,增加gc回收效率,这样一来当强引用不存在时,弱引用也将被回收。 -
Thread中value的内存泄漏
虽然ThreadLocalMap中Entry的key因为被设计为弱引用可以被GC回收,但是由于Entry节点中还引用着Value的强引用。因此在不做任何操作的情况下GC还是无法对value进行回收。因此在使用ThreadLocal后务必使用ThreadLocal.remove()方法清空线程本地变量
-
ThreadLocal对内存泄漏的优化
1、set方法哈希冲突时会替换失效的entry节点(即key已经被回收,但是value的引用未被释放)
2、新增Entry节点时也会替换掉部分失效的entry节点。(只是对内存泄漏后的场景做了优化处理,但还是未完全避免此问题。)
-
多线程共享的ThreadLocal对象(静态变量)是如何做到线程隔离的
ThreadLocal其本身只不过是堆中的一个普通对象而已。
当存在其他线程调用静态ThreadLocal的get或者set方法时,会初始化一个ThreadLocalMap并设置到当前线程(Thread对象)的threadLocals变量中,ThreadLocalMap内部是由ThreadLocal及对应值组成的Entry节点数组
调用get方法时,当前线程的threadLocals若为null,则会初始化一个ThreadlocalMap,value为初始值(默认是null,若重写了initvalue方法则初始值为initvalue方法返回的默认值)
调用set时,当前线程的threadLocals若为null,也会初始化一个ThreadLocalMap对象,value为当前set的值
ThreadLocal其实相当于是一个用于获取Value的凭证,作为静态变量的ThreadLocal相当于是多个线程使用同一个凭证而已。至于这个凭证能够取到什么数据还要看具体的线程而定。同样的假如凭证丢失了(GC掉了),那么此凭证对应的Value也就无法被取出了。
总结
ThreadLocal内部其实并不保存数据,数据保存在Thread内部的threadlocals中,ThreadLocal仅仅作为ThreadLocalMap的key来取数据而已。
因此当代码中ThreadLocal的强引用被置为null时,ThreadLocal对象将变为不可达对象,因为ThreadLocal不可达,所以ThreadLocal对应的Value数据也将无法被取出。
所以ThreadLocalMap将ThreadLocal设置为弱引用避免key的内存泄漏。但是若存在线程复用情况时,由于ThreadLocalMap中的Entry数组还持有value的强引用因此value将无法进行回收导致内存泄漏(value内存泄漏)。