什么是ThreadLocal?ThreadLocal是一个本地线程复制变量工具类。主要用于映射私有线程和线程中存储的复制对象,线程之间的变量互不干扰。在高并发场景下,可以实现无状态调用,尤其是在每个线程都依赖不合理的变量值来完成操作的场景下。
ThreadLocal特点:就是把一个数据放在一个线程里,不管中间执行什么操作。只要你想获取,就可以通过调用get来获取保存的数据。
ThreadLocal内部结构图
从上面的结构图可以看出ThreadLocal的核心机制
每个线程内部都有一个映射。该映射存储线程本地对象(键)和线程的变量副本(值)。线程内部的映射由ThreadLocal设置,负责从映射中获取和设置线程的变量值。线程Thread内部的映射在类中描述如下:
为什么ThreadLocal会泄漏内存?我们先来分析一下ThreadLocalMap。
我们可以知道每个线程都维护一个ThreadLocalMap。这个映射表的关键是ThreadLocal实例本身,值是真正需要存储的对象。也就是说,ThreadLocal本身并不存储值,只是作为线程从ThreadLocalMap获取值的一个键。仔细看ThreadLocalmap。这个映射使用ThreadLocal的弱引用作为键,弱引用的对象将在GC中被回收。
这样,当threadlocal变量设置为null时,就没有对threadlocal实例的强引用,所以threadlocal将被gc回收。这样,ThreadLocalMap中就会有空键的条目,没有办法访问这些空键条目的值。如果当前线程被延迟,那么这些带有空键的条目的值总会有一个强引用链:Thread Ref-ThrealocalMap-Entry-value,这个值永远不会被访问,所以存在内存泄漏。
其实java开发人员也考虑到了这个问题,所以在获取和设置的时候,调用了expungeStaleEntry方法来清除条目中带有null Key的值。但是这并不及时,也不会每次都执行,所以在某些情况下还是会发生内存泄漏。remove方法中只显式调用expungeStaleEntry方法。
我们来看看ThreadLocal的get方法的实现:
让我们继续看看map.getEntry方法。
当键为null时,调用getEntryAfterMiss方法
当key为null时,调用expungeStaleEntry方法。
可能有人会疑惑,用上面的方法,为什么会造成内存泄露呢?
一般我们设置的ThreadLocal设置为static,static变量可以作为GCRoot的根节点,所以总会有初始化ThreadLocal和调用set和get而不调用remove方法造成的内存泄漏。例如,Get方法只会在ThreadLocalMap中没有必需的键时调用clear方法。