TheadLocal的基本使用及其内存泄漏风险

793 阅读3分钟

ThreadLocal是Java所提供的"线程封闭的"一种机制,其会为每个线程都创建一份变量的副本,以保障线程安全。

其目的与synchronized完全不一致synchronized保证了多线程环境下数据的一致性,而ThreadLocal却是保证了多线程环境下数据的独立性。

有两种方式可以构造一个ThreadLocal对象。

第一种是直接通过new的方式,如下:

ThreadLocal<String> t = new ThreadLocal();

或者可以通过静态方法ThreadLocal#withInitial(Suppiler)来构造:

ThreadLocal<String> t = ThreadLocal.withInitial(() -> "initial value");

这种方式实际上构造的是一个SuppliedThreadLocal对象。该类是ThreadLocal的一个子类。其内部保存静态方法所传入的suppiler函数用于**在从未设置****value**的情况下能够让调用者获取到一个初始化的值

ThreadLocal构建完毕之后,就可以通过set()get()remove()来操作对应的值了。

t.set("another value"); // 设置ThreadLocal的值
String value = t.get(); // 获取ThreadLocal的值
t.remove(); // 删除ThreadLocal的值

ThreadLocal内部原理

在每个Thread对象中,都有一个类型为ThreadLocal.ThreadLocalMap的对象,名为threadLocals。这是真正存储ThreadLocal值的地方,并且,该字段由ThreadLocal维护,而非Thread对象本身管理。

ThreadLocalMap内部维护了一个Entry类型的数据,名为tableEntryThreadLocalMap的静态内部类,其类定义如下:

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

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

Entry是一个K:V结构的类,其中KeyThreadLocal对象,且是一个弱引用WeakReference)对象,Value是使用者所设置的值。

根据以上信息可知,ThreadLocal整体结构如下:

image (1).png

内存泄漏风险

ThreadLocal的底层存储结构ThreadLocalMap.Entry中,ThreadLocal 对象作为Key且以弱引用的形式存在。

弱引用具备如下特性:

通过WeakReference类来使用,在没有其他强引用关系的情况下,弱引用对象会在下一次垃圾收集时回收掉

通过上图可知,一般情况下,只有Defined Class(指开发者所定义的,实例化ThreadLocal的类)的对应对象与ThreadLocal对象存在直接的强引用关系。也就是说当Defined Class实例被GC回收之后,就很容易造成ThreadLocalMap.EntryKey的内存空间被回收,从而变为null值。

image (2).png

现代应用大量使用池化技术来复用线程,线程对象不一定能够被回收。在这样的情况下,Value存在"Thread —> TheadLocalMap —> ThreadLocalMap.Entry —> Value"的强引用关系链,也无法被回收。但在其Key 被回收的情况下,开发者永远也无法访问到这个Value值,从而导致了内存泄漏。

内存泄漏解决方案

ThreadLocalMap中存在一个名为cleanSomeSlots的方法,其会扫描KeynullEntry,并将其删除掉。

这个方法在调用ThreadLocalget() 、set(Object) 和remove()方法时都会被调用到。因此,在大部分的场景下,ThreadLocal出现内存泄漏的几率已经比较小了。

不过我们也可以通过两个方法来"加强防御"。

第一种方法考虑将ThreadLocal对象设置为静态对象。

public class Main {
  
  public static ThreadLocal<String> t = new ThreadLocal();
}

此时,ThreadLocalClass对象产生了强引用关系,除非发生"类型卸载",ThreadLocal对象都不会被回收掉。

这种方式会使用ThreadLocalMap.EntryKeyValue都不会被回收掉,且能够被指定的线程所访问到。但缺点也很明显。若ThreadLocal所存储的数据本就只是临时使用,那"永不回收"的Key 和Value实际上就成了另一种"内存泄漏"。

更好的方式是手动回收,即使用之后手动调用ThreadLocal#remove()方法。

ThreadLocal<String> t = new ThreadLocal<>()

try {
  t.set(obj);
  doSomething();
} finally {
  t.remove();
}

这种方式虽然不够灵活,但却是目前使用ThreadLocal最为推荐的方式。