原因分析
内存溢出的问题和解决方案比较简单,重点在于“原因分析”,我们要通过内存溢出的问题搞清楚,为什么 ThreadLocal 会这样?是什么原因导致了内存溢出?
要搞清楚这个问题(内存溢出的问题),我们需要从 ThreadLocal 源码入手,所以我们首先打开 set 方法的源码(在示例中使用到了 set 方法),如下所示:
public void set(T value) {
// 得到当前线程
Thread t = Thread.currentThread();
// 根据线程获取到 ThreadMap 变量
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value); // 将内容存储到 map 中
else
createMap(t, value); // 创建 map 并将值存储到 map 中
}
从上述代码我们可以看出 Thread、ThreadLocalMap 和 set 方法之间的关系:每个线程 Thread 都拥有一个数据存储容器 ThreadLocalMap,当执行 ThreadLocal.set 方法执行时,会将要存储的值放到 ThreadLocalMap 容器中,所以接下来我们再看一下 ThreadLocalMap 的源码:
staticclass ThreadLocalMap {
// 实际存储数据的数组
private Entry[] table;
// 存数据的方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 如果有对应的 key 直接更新 value 值
if (k == key) {
e.value = value;
return;
}
// 发现空位插入 value
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 新建一个 Entry 插入数组中
tab[i] = new Entry(key, value);
int sz = ++size;
// 判断是否需要进行扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
// ... 忽略其他源码
}
从上述源码我们可以看出:ThreadMap 中有一个 Entry[] 数组用来存储所有的数据,而 Entry 是一个包含 key 和 value 的键值对,其中 key 为 ThreadLocal 本身,而 value 则是要存储在 ThreadLocal 中的值。
根据上面的内容,我们可以得出 ThreadLocal 相关对象的关系图,如下所示:

也就是说它们之间的引用关系是这样的:Thread -> ThreadLocalMap -> Entry -> Key,Value,因此当我们使用线程池来存储对象时,因为线程池有很长的生命周期,所以线程池会一直持有 value 值,那么垃圾回收器就无法回收 value,所以就会导致内存一直被占用,从而导致内存溢出问题的发生。