youyichannel

志于道,据于德,依于仁,游于艺!

0%

为什么每次使用完 ThreadLocal 都要调用 remove() 方法呢?

首先需要明确的是这件事和内存泄露相关。

内存泄露

内存泄露指的是当某一个对象不再使用时,其占用的内存无法回收的情况。

因为通常情况下,当一个对象不再使用(有用)的情况下,垃圾回收起 GC 应该将这部分内存回收掉,这样的话就可以让这部分内存被重新使用,否则,如果对象没有用却不能回收,这样只会导致垃圾对象积压,以至于可用内存越来越少,最后出现 OOM 的错误。

Thread、ThreadLocal、ThreadLocalMap 的关系

一个 Thread 中只有一个 ThreadLocalMap,而在一个 ThreadLocalMap 中可以有很多 ThreadLocal,每个 ThreadLocal 对应一个 Value。因为一个 Thread 是可以调用多个 ThreadLocal 的,所以 Thread 内部就采用了 ThreadLocalMap 这样 Map 的数据结构来存放 ThreadLocal 和 value。

ThreadLocalMap 的结构

static class ThreadLocalMap {

static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// ...
private Entry[] table;

// ...
}

ThreadLocalMap 类是 Thread 类中的静态内部类,其中最重要的就是 Entry 内部类。在 ThreadLocalMap 中有一个 Entry 类型的数组,可以将其理解成一个 Map,其中键值对为:

  • 键:当前的 ThreadLocal;
  • 值:实际需要存储的变量。

ThreadLocalMap 类似于 Map,和 HashMap 类似,有 setget 等一系列操作 API,具体实现时有些许不同。比如 ThreadLocalMap 解决 Hash 冲突是采用线性探测法,HashMap 是采用拉链法。

Key 泄露

根据 ThreadLocal 的内部结构可以知道,每一个 Thread 都有一个 ThreadLocalMap 的成员变量,线程在访问 ThreadLocal 之后,会在它的 ThreadLocalMap 中的 Entry 中去维护该 ThreadLocal 变量和具体实例的映射。

现在我们在代码中执行了 ThreadLocal instance = null 这样的操作,想要 GC 清理掉该 ThreadLocal 实例,但是假设我们在 ThreadLocalMap 的 Entry 中强引用了 ThreadLocal 实例,那么,即使在代码中将 ThreadLocal 实例置空了,但是 Thread 类中依然会有引用链存在。于是乎,GC 在垃圾回收的时候会进行可达性分析,它就会发现这个 ThreadLocal 对象依然是可达的,所以就不会对这个 ThreadLocal 对象进行垃圾回收,这样的话就造成了内存泄露。

幸运的是,这种情况 JDK 已经考虑在内了,ThreadLocalMap 中的 Entry 继承了 WeakReference 弱引用。

static class Entry extends WeakReference<ThreadLocal<?>> { /* .., */ }

弱引用的特点:如果某个对象只被弱引用关联,而没有任何强引用关联,那么这个对象就可以被回收,所以弱引用不会阻止 GC。因此,弱引用的机制就避免了 ThreadLocal Key 的内存泄露问题。

Value 泄露

虽然 ThreadLocalMap 中的每个 Entry 都是一个对 Key 的弱引用,但是这个 Entry 包含了一个对于 value 的强引用,也就是 value = v 这行代码,代表了强引用的指向。

正常情况下,当线程终止时,key 所对应的 value 是可以被正常垃圾回收的,因为此时没有任何强引用存在了。但是不排除有时线程的生命周期很长,线程迟迟不终止,那么可能 ThreadLocal 以及它所对应的 value 早就不再有用了,依旧没有被回收。

「引用链路」

重点关注 CurrentThread Ref → CurrentThread → ThreadLocalMap → Entry → Value → 可能泄漏的 value 实例 这条链路,这条链路是随着线程的存在而一直存在的,如果线程执行耗时任务不停止,那么当垃圾回收器进行可达性分析的时候,Value 是可达的,所以不会被回收。但是可能此时代码已经执行完业务逻辑了,不再需要该 Value 了,此时也就发生了内存泄露问题。

JDK 同样也考虑了这个问题,在执行 ThreadLocal 的 setremove 等方法时,都会扫描 key 为 null 的 Entry,如果发现某个 Entry 的 key 为 null,则代表它所对应的 value 也没有作用了,就会把对应的 value 置空,这样,value 对象就可以被正常回收了。但是假设 ThreadLocal 已经不再被使用了,那么实际上这些方法也不会被调用了,与此同时,如果这个线程又一直存活、不终止的话,上述引用链就一直存在,也就导致了 value 的内存泄露。

如何避免内存泄露?

调用 ThreadLocal 的 remove 方法。调用该方法就可以删除对应的 value 对象,就可以避免内存泄漏。

// ThreadLocal#remove
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}

该方法先获取了 ThreadLocalMap 引用,并且调用了它的 remove 方法,该方法可以把 key 对应的 value 清理掉,这样一来,value 就可以被 GC 了。

因此,在使用完 ThreadLocal 之后,我们应该手动的调用 remove 方法,目的就是为了防止内存泄露。