优质博文:IT-BLOG-CN
从 ThreadLocalMap看 ThreadLocal使用不当的内存泄漏问题
【1】基础概念 : 首先我们先看看ThreadLocalMap
的类图,我们知道 ThreadLocal
只是一个工具类,他为用户提供get
、set
、remove
接口操作实际存放本地变量的threadLocals
(调用线程的成员变量),也知道 threadLocals
是一个ThreadLocalMap
类型的变量,下面我们来看看ThreadLocalMap
这个类。在此之前,我们回忆一下Java
中的四种引用类型链接
【2】分析ThreadLocalMap
内部实现: 我们知道ThreadLocalMap
内部实际上是一个Entry
数组private Entry[] table
,我们先看看Entry
的这个内部类
java
/**
* 是继承自WeakReference的一个类,该类中实际存放的key是指向ThreadLocal的弱引用和与之对应的value值(该value值
* 就是通过ThreadLocal的set方法传递过来的值)由于是弱引用,当get方法返回null的时候意味着回收引用
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** value就是和ThreadLocal绑定的 */
Object value;
//k:ThreadLocal的引用,被传递给WeakReference的构造方法
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//WeakReference构造方法(public class WeakReference<T> extends Reference<T> )
public WeakReference(T referent) {
super(referent); //referent:ThreadLocal的引用
}
//Reference构造方法
Reference(T referent) {
this(referent, null);//referent:ThreadLocal的引用
}
Reference(T referent, ReferenceQueue<? super T> queue) {
this.referent = referent;
this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
}
在上面的代码中,我们可以看出,当前ThreadLocal
的引用k
被传递给WeakReference
的构造函数,所以ThreadLocalMap
中的key
为ThreadLocal
的弱引用。当一个线程调用ThreadLocal
的set
方法设置变量的时候,当前线程的 ThreadLocalMap
就会存放一个记录,这个记录的key
值为ThreadLocal
的弱引用,value
就是通过set
设置的值。如果当前线程一直存在且没有调用该ThreadLocal
的remove
方法,如果这个时候别的地方还有对ThreadLocal
的引用,那么当前线程中的ThreadLocalMap
中会存在对ThreadLocal
变量的引用和 value
对象的引用,是不会释放的,就会造成内存泄漏。
考虑这个ThreadLocal
变量没有其他强依赖,如果当前线程还存在,由于线程的ThreadLocalMap
里面的key
是弱引用,所以当前线程的ThreadLocalMap
里面的ThreadLocal
变量的弱引用在gc
的时候就被回收,但是对应的value
还是存在的这就可能造成内存泄漏(因为这个时候 ThreadLocalMap
会存在key
为 null
但是value
不为null
的entry
项 )。
ThreadLocalMap
中的Entry
的key
使用的是ThreadLocal
对象的弱引用,在没有其他地方对ThreadLocal
依赖,ThreadLocalMap
中的ThreadLocal
对象就会被回收掉,但是对应的value
不会被回收,这个时候Map
中就可能存在key
为null
但是value
不为null
的项,这需要实际使用的时候使用完毕及时调用remove
方法避免内存泄漏。
如果使用线程池,由于线程可能并不是真正的关闭(比如newFixedThreadPool
会保持线程一只存活)。因此,如果将一些大对象存放到ThreadLocalMap
中,可能会造成内存泄漏。因为线程没有关闭,无法回收,但是这些对象不会再被使用了。如果希望及时回收对象,则可以使用Thread.remove()
方法将变量移除。
java
ThreadLocal<Object> threadLocal = new ThreadLocal<>();
// 存储数据
threadLocal.set(someData);
// 使用完毕后清除
threadLocal.remove();
我们再看下ThreadLocal
底层的源码:
java
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap变量
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
remove()
方法逻辑比较简单,首先获取当前线程的ThreadLocalMap
对象,然后循环遍历key
,将目标key
以及对应的value
都设置为null
。
java
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
// 循环遍历目标key,然后将key和value都设置为null
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
// 清理value值
expungeStaleEntry(i);
return;
}
}
}
使用try-with-resources
或try-finally
块:如果你的ThreadLocal
变量在需要清理的资源管理上下文中使用,可以使用try-with-resources
(自动清理)或try-finally
(手动清理)块来确保及时清理。
java
try (ThreadLocalResource resource = new ThreadLocalResource()) {
// 使用 ThreadLocalResource
}
// 或者使用 try-finally
ThreadLocalResource resource = new ThreadLocalResource();
try {
// 使用 ThreadLocalResource
} finally {
resource.close(); // 在 close 方法中清理 ThreadLocal 变量
}
使用InheritableThreadLocal
:如果需要在子线程中访问父线程的ThreadLocal
变量,并且确保在子线程中正确清理,可以考虑使用InheritableThreadLocal
。这个类允许子线程继承父线程的ThreadLocal
变量,并在子线程完成后自动清理。
java
ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
threadLocal.set("Hello, Parent Thread");
Runnable childTask = () -> {
String value = threadLocal.get(); // 子线程可以访问父线程的 ThreadLocal 变量
// ...
};
Thread childThread = new Thread(childTask);
childThread.start();