深入剖析 ThreadLocal 实现原理以及内存泄漏问题

一、概述

在 2017 京东校园招聘笔试题中遇到了描述 ThreadLocal 的实现原理和内存泄漏的问题,之前看过 ThreadLocal 的实现原理,但是网上有很多文章将的很乱,其中有很多文章将 ThreadLocal 与线程同步机制混为一谈,特别注意的是 ThreadLocal 与线程同步无关,并不是为了解决多线程共享变量问题!
ThreadLocal 官网解释:

  This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable.  {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g.,a user ID or Transaction ID)

->翻译过来的大概意思就是:ThreadLocal 类用来提供线程内部的局部变量。这些变量在多线程环境下访问(通过 get 或 set 方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量,ThreadLocal 实例通常来说都是 private static 类型。
总结:ThreadLocal 不是为了解决多线程访问共享变量,而是为每个线程创建一个单独的变量副本,提供了保持对象的方法和避免参数传递的复杂性。

ThreadLocal 的主要应用场景为按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。例如:同一个网站登录用户,每个用户服务器会为其开一个线程,每个线程中创建一个 ThreadLocal,里面存用户基本信息等,在很多页面跳转时,会显示用户信息或者得到用户的一些信息等频繁操作,这样多线程之间并没有联系而且当前线程也可以及时获取想要的数据。

二、实现原理

ThreadLocal 可以看做是一个容器,容器里面存放着属于当前线程的变量。ThreadLocal 类提供了四个对外开放的接口方法,这也是用户操作 ThreadLocal 类的基本方法:
(1) void set(Object value)设置当前线程的线程局部变量的值。
(2) public Object get()该方法返回当前线程所对应的线程局部变量。
(3) public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是 JDK 5.0 新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
(4) protected Object initialValue()返回该线程局部变量的初始值,该方法是一个 protected 的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第 1 次调用 get()或 set(Object)时才执行,并且仅执行 1 次,ThreadLocal 中的缺省实现直接返回一个 null。

可以通过上述的几个方法实现 ThreadLocal 中变量的访问,数据设置,初始化以及删除局部变量,那 ThreadLocal 内部是如何为每一个线程维护变量副本的呢?

其实在 ThreadLocal 类中有一个静态内部类 ThreadLocalMap(其类似于 Map),用键值对的形式存储每一个线程的变量副本,ThreadLocalMap 中元素的 key 为当前 ThreadLocal 对象,而 value 对应线程的变量副本,每个线程可能存在多个 ThreadLocal。

源代码:

/**
 Returns the value in the current thread's copy of this
 thread-local variable.  If the variable has no value for thecurrent thread, it is first initialized to the value returned by an invocation of the {@link #initialValue} method.
  @return the current thread's value of this thread-local
 */
public T get() {
    Thread t = Thread.currentThread();//当前线程
    ThreadLocalMap map = getMap(t);//获取当前线程对应的ThreadLocalMap
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);//获取对应ThreadLocal的变量值
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();//若当前线程还未创建ThreadLocalMap,则返回调用此方法并在其中调用createMap方法进行创建并返回初始值。
}
//设置变量的值
public void set(T value) {
   Thread t = Thread.currentThread();
   ThreadLocalMap map = getMap(t);
   if (map != null)
       map.set(this, value);
   else
       createMap(t, value);
}
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;
}
/**
为当前线程创建一个ThreadLocalMap的threadlocals,并将第一个值存入到当前map中
@param t the current thread
@param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
//删除当前线程中ThreadLocalMap对应的ThreadLocal
public void remove() {
       ThreadLocalMap m = getMap(Thread.currentThread());
       if (m != null)
           m.remove(this);
}

上述是在 ThreadLocal 类中的几个主要的方法,他们的核心都是对其内部类 ThreadLocalMap 进行操作,下面看一下该类的源代码:

static class ThreadLocalMap {
  //map中的每个节点Entry,其键key是ThreadLocal并且还是弱引用,这也导致了后续会产生内存泄漏问题的原因。
 static class Entry extends WeakReference> {
           Object value;
           Entry(ThreadLocal k, Object v) {
               super(k);
               value = v;
   }
    /**
     * 初始化容量为16,以为对其扩充也必须是2的指数 
     */
    private static final int INITIAL_CAPACITY = 16;
    /**
     * 真正用于存储线程的每个ThreadLocal的数组,将ThreadLocal和其对应的值包装为一个Entry。
     */
    private Entry[] table;

    ///....其他的方法和操作都和map的类似
}

总之,为不同线程创建不同的 ThreadLocalMap,用线程本身为区分点,每个线程之间其实没有任何的联系,说是说存放了变量的副本,其实可以理解为为每个线程单独 new 了一个对象。

三、内存泄漏问题(参考其他博文)

  在上面提到过,每个 thread 中都存在一个 map, map 的类型是 ThreadLocal.ThreadLocalMap. Map 中的 key 为一个 threadlocal 实例. 这个 Map 的确使用了弱引用, 不过弱引用只是针对 key. 每个 key 都弱引用指向 threadlocal. 当把 threadlocal 实例置为 null 以后, 没有任何强引用指向 threadlocal 实例, 所以 threadlocal 将会被 gc 回收. 但是, 我们的 value 却不能回收, 因为存在一条从 current thread 连接过来的强引用. 只有当前 thread 结束以后, current thread 就不会存在栈中, 强引用断开, Current Thread, Map, value 将全部被 GC 回收.
  所以得出一个结论就是只要这个线程对象被 gc 回收,就不会出现内存泄露,但在 threadLocal 设为 null 和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的。就可能出现内存泄露。