起因
网上一系列的文章都在分析ThreadLocal,说如果线程不销毁的话,value会一直存在于内存中,所以必须调用remove,下面通过一些实践和源码来分析一下这个观点是否在所有情况下都适用
案例分析
第一个实验
可以看到上面这张图片,在使用线程池时,由于线程被复用没有销毁,在没有调用remove的情况下会读取到上一次的状态,所以这也就告诉我们在线程不销毁的情况下,值不会被回收。
第二个实验
首先设置了-Xms100m -Xmx100m,然后使用了如下的代码
public class ThreadlocalApplication {
public static ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
SpringApplication.run(ThreadlocalApplication.class, args);
ExecutorService exec = Executors.newFixedThreadPool(99);
for (int i = 0; i < 1000; i++) {
exec.execute(() -> {
threadLocal.set(new byte[1024 * 1024]);
try {
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
threadLocal.remove();
}
});
}
}
}
运行上面的代码没有出现任务问题,但是将 threadLocal.remove() 注释掉以后就出现了内存溢出的问题,原因是1m的数组没有被及时回收,这也从侧面证明了手动 remove() 的必要性
源码分析
主要看一下get方法
public T get() {
// 获取当前线程
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// map存在时,获取value,getEntry中会判断key是不是为null
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 未获取到值,初始化
return setInitialValue();
}
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
// 当值为null或者key为null时进行处理
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
// 清除value,设置为null
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
从get方法的一系列逻辑我们可以看出,即使使用线程池,在每次get时也会将key为null的值清除掉。
结论
综上所述,内存泄漏应该只会存在于线程池数量较大且存储在ThreadLocal中的数据量较大时,但是手动调用 remove() 可以加快内存的释放,所以还是推荐手动调用的。
如有错误欢迎在评论中指出,和我一起讨论。