ThreadLocal会产生内存泄露的原因是什么

1,965 阅读3分钟

通常情况下,我们在内存中创建的变量是可被多个线程同时访问的,Java通过ThreadLocal实现了线程数据隔离的机制。

ThreadLocal的用法

既然前面提到了ThreadLocal存储的变量是线程隔离的,我们不妨就测试一下是否如我们所说。我们先创建两个线程,然后为这两个线程设置ThreadLocal变量。

public class ThreadLocalDemo {
 
 private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
 
 public static void main(String[] args) {
  Thread t1 = new Thread(() -> {
   threadLocal.set("this is t1");
   try {
    TimeUnit.SECONDS.sleep(1);
   } catch (InterruptedException e) {
    e.printStackTrace();
   }
   System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
  }, "t1");
  Thread t2 = new Thread(() -> {
   threadLocal.set("this is t2");
   try {
    TimeUnit.SECONDS.sleep(1);
   } catch (InterruptedException e) {
    e.printStackTrace();
   }
   System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());
  }, "t2");
  t1.start();
  t2.start();
 }
}

上面的代码输出下面两行结果,说明ThreadLocal的变量确实是线程间隔离的。

t1: this is t1
t2: this is t2

ThreadLocal的原理

开始讲ThreadLocal原理之前,我们需要简单了解一下ThreadLocalMap。在Thread类中有个threadLocals变量,这是一个ThreadLocal.ThreadLocalMap类型的变量,也就是说ThreadLocalMap是ThreadLocal的静态内部类。从名字可以看出ThreadLocalMap同样是一个Map,Map的key是ThreadLocal对象,这里的ThreadLocal对象是一个弱引用对象,也就是说每当发生GC,ThreadLocal对象就会被回收。

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

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

我们来看一下ThreadLocal中的set(T value)方法的具体实现。

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

首先是获取当前线程,根据当前线程拿到threadLocals,如果这个变量存在就为它设置一个值,否则就初始化threadLocals。整体的思路我们通过流程图来捋一下。

总结一下上面说的内容,Thread类维护着一个ThreadLocalMap对象,ThreadLocalMap是ThreadLocal的静态内部类并且ThreadLocalMap中的Entry的key是ThreadLocal对象的弱引用,value是要存储的内容,大致是下面这样的关系。

根据上面的内容我们再看一下get()的方法体。

public T get() {
    Thread t = Thread.currentThread();
    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();
}

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

protected T initialValue() {
    return null;
}

看看get()方法是不是豁然开朗?同样是获取当前线程,然后通过线程拿到ThreadLocalMap对象,再通过ThreadLocalMap对象获取存储的值。当然如果ThreadLocalMap对象不存在那就去初始化一下。

ThreadLocal是怎么内存泄漏的

由于ThreadLocalMap中的key是ThreadLocal的弱引用,一旦发生GC便会回收ThreadLocal,那么此时的ThreadLocalMap存储的key便是null。如果不通过手动remove()那么ThreadLocalMap的Entry便伴随线程的整个生命周期造成内存泄漏,大致就是一个thread ref -> thread -> threadLocals -> entry -> value的强引用关系。因此Java其实是有对于内存泄漏的一些预防机制的,每次调用ThreadLocal的set()get()remove()方法时都会回收key为空的Entry的value。

那么为什么ThreadLocalMap的key要设计成弱引用呢?其实很简单,如果key设计成强引用且没有手动remove(),那么key会和value一样伴随线程的整个生命周期,如果key是弱引用,被GC后至少ThreadLocal被回收了,在下一次的set()get()remove()还会回收key为null的Entry的value。

喜欢本文的朋友欢迎关注我的公众号:SKY技术修炼指南