基本原理
每一个线程Thread中都有一个变量ThreadLocalMap。
ThreadLocal的全部操作都是基于Thread. ThreadLocalMap进行操作的。
ThreadLocalMap是一个Entry数组,Entry储存Key(WeakReference<ThreadLocal<?>>)和Value(Object)。
Set过程
-
根据当前线程Thread获取其内部的ThreadLocalMap对象。
-
如果是第一次设置值,ThreadLocalMap对象是空值,所以会进行初始化操作,即调用createMap(t,value)方法
-
如果不是第一次,就将ThreadLocal对象包装为弱引用对象作为Key,所设的值为value,构造Entry对象,添加到Entry数组中去。
-
set方法中获取key的对应的索引值,并从此位置进行遍历,nextIndex每次将i+1。当有相同的key时,则替换旧值。如果遍历之后无相同key,则创建新值。所以
ThreadLocal处理hash碰撞的方法是使用环形数组,进行线性探测法,依次检测下一个位置。
get过程
-
根据当前线程,首先获取ThreadLocalMap对象
-
由于ThreadLocalMap使用的当前的ThreadLocal作为key,所以传入的参数为this,然后调用getEntry()方法,通过这个key构造索引,根据索引去table(Entry数组)中去查找线程本地变量,找到Entry对象,然后判断Entry对象e不为空并且e的引用与传入的key一样则直接返回。
-
如果找不到则调用getEntryAfterMiss()方法。调用getEntryAfterMiss表示直接散列到的位置没找到,那么顺着hash表递增(循环)地往下找,从i开始,一直往下找,直到找到目标对象或者null。
为什么要使用WeakReference<ThreadLocal<?>>
具有弱引用的对象拥有短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
如果不使用弱引用,因为Entry对象包含ThreadLocal对象作为key,所以Entry对象会包含ThreadLocal对象的强引用,所以即使在外层将ThreadLocal=null,也无法回收ThreadLocal对象。使用弱引用,当外层ThreadLocal=null时,Entry对象只包含ThreadLocal对象的虚引用,可以直接被GC回收。 当把ThreadLocal对象的引用置为null后,没有任何强引用指向内存中的ThreadLocal实例,threadLocals的key是它的弱引用,故它将会被GC回收。
但线程中threadLocals里的value却没有被回收,因为存在着一条从Entry对象连接过来的强引用,且因为无法再通过ThreadLocal对象的get方法获取到这个value,它永远不会被访问到了,所以还存在内存泄漏问题。
内存泄漏问题
每个thread中都存在一个map,map的类型的ThreadLocal,ThreadLocalMap,Map中的key为一个threadlocal的实例,这个map的确使用了弱引用,不过弱引用只是针对key,每个key都弱引用指向threadlocal。当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal会被gc回收。但是,我们的value却不能被回收,因为存在一条从current thread连接过来的强引用(Entry数组中的Entry对象强引用value对象)。只有当前thread结束以后,current thread就不会存在栈中,强引用断开。Current Thread,Map,value将全部被gc回收。
只要这个线程对象被gc回收,就不会出现内存泄漏,但是在ThreadLocal设为null和线程结束这段时间不会被回收的。如果线程暂时不回收,这就发生了真正意义上的内存泄漏。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄漏。
Java为了最小化减少内存泄漏的可能性和影响,在ThreadLocal的get,set的时候都会清除线程Map里所有key为null的value。所以最坏的情况就是threadlocal对象设null了,开始发生内存泄漏,然后使用线程池。这个线程结束,线程放回线程池中不销毁,这个线程一直不被使用,或者分配使用了又不再调用get,set方法,那么这个期间就会发生真正的内存泄漏。