深入理解 ThreadLocal

Published: Creative Commons Licence

1.基础概念

ThreadLocal ,顾名思义就是用来提供线程(Thread)内部的局部(Local)变量的,主要应用场景为在同一个线程内方便地共享变量。

例如:一次用户请求,服务器会为其开一个线程,我们在线程中创建一个 ThreadLocal,里面存请求上下文信息,这样在之后获取这些信息时就可以很方便地拿到,而不用层层显式传参。

2.实现原理

ThreadLocal 的实现原理并不复杂,核心就是在 Thread 类里维护了一个 Map:

/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

然后通过 ThreadLocal 的接口读写这个变量:

/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param  t the current thread
* @return the map
*/

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

/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @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 应用场景定制的一个 HashMap,实现细节可以先不去管,Key 为 ThreadLocal 的实例,Value 为线程局部变量。所以说,ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。

准备工作做好了,接下来看下 ThreadLocal 是怎么实现线程局部变量的读写的,核心代码也都很简单。

  • get:其实就是去读取 map 里的值。
/**
* Returns the value in the current thread's copy of this
* thread-local variable.  If the variable has no value for the
* current 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);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

当 map 为空或者没有读到值时,会触发初始化逻辑:

/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial 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;
}

initialValue() 是一个可重写的接口,用于让开发者自定义想要的初始值。

/**
* Returns the current thread's "initial value" for this
* thread-local variable.  This method will be invoked the first
* time a thread accesses the variable with the {@link #get}
* method, unless the thread previously invoked the {@link #set}
* method, in which case the {@code initialValue} method will not
* be invoked for the thread.  Normally, this method is invoked at
* most once per thread, but it may be invoked again in case of
* subsequent invocations of {@link #remove} followed by {@link #get}.
*
* <p>This implementation simply returns {@code null}; if the
* programmer desires thread-local variables to have an initial
* value other than {@code null}, {@code ThreadLocal} must be
* subclassed, and this method overridden.  Typically, an
* anonymous inner class will be used.
*
* @return the initial value for this thread-local
*/
protected T initialValue() {
    return null;
}
  • set:其实就是往 map 里面放值。
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value.  Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
*        this thread-local.
*/
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
  • remove:用于使用完毕时清除数据,务必调用,不然会出现内存泄漏问题,见下文
/**
* Remove the entry for key.
*/
private void remove(ThreadLocal<?> key) {
    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)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

3.内存泄漏问题

所谓内存泄漏,就是已经无法访问的内存却没有释放,ThreadLocal 使用不当时会导致内存泄漏问题。

首先看一下 ThreadLocal 运行时的内存示意图:

ThreadLocal 运行时内存示意图

ThreadLocal.ThreadLocalMap的 Key 实现是弱引用,也即图中的虚线。弱引用不会阻止 GC,因此考虑下面的情况:

  • ThreadLocalRef 被清除了,堆中的 ThreadLocal 实例不存在强引用了,被 GC 回收。

  • ThreadLocalMap 里出现了一条 Key 为 null 的 Entry,后续无法读写,也无法回收,造成内存泄漏。

如果当前线程结束后被销毁,则这一块内存可以被释放;但是如果是线程池的模式,线程迟迟不结束的话,这个问题就会一直存在。

其实,ThreadLocalMap 的设计中已经考虑到这种情况,也加上了一些防护措施:在 ThreadLocal 的 get(), set(), remove() 的时候都会清除线程 ThreadLocalMap 里所有 key 为 null 的 value。

但这样也无法完全避免内存泄漏,因为可能上游再也不会调用get(),set(),remove()方法了,参考文章里也给出了一个 Tomcat 内存泄漏的实例:ThreadLocal 内存泄漏的实例分析

正确的处理方式是:每次使用完 ThreadLocal,都调用它的 remove() 方法清除数据 ,这样才能从根源上避免内存泄漏问题。

4.应用实战

下面以在一次请求中透传上下文信息为例,来实际演示 ThreadLocal 用法。

首先创建一个类来管理 ThreadLocal 实例:

public class ContextInfoThreadLocal {

    private static final ThreadLocal<ContextInfo> CONTEXT_INFO_THREAD_LOCAL = new ThreadLocal<>();

    public static void set(ContextInfo contextInfo) {
        CONTEXT_INFO_THREAD_LOCAL.set(contextInfo);
    }

    public static ContextInfo get() {
        return CONTEXT_INFO_THREAD_LOCAL.get();
    }

    public static void remove() {
        CONTEXT_INFO_THREAD_LOCAL.remove();
    }
}

然后在请求的入口处把上下文信息放进去,最好使用AOP的方式:

    @Around("pointcut()")
    public Object around(ProceedingJoinPoint point) {
        try {
            ContextInfo ContextInfo = getContextInfo(point.getArgs());
            // 把 contextInfo 信息放入ThreadLocal
            ContextInfoThreadLocal.set(contextInfo);
            return point.proceed(point.getArgs());
        } catch (Throwable t) {
            // ...
        } finally {
            ContextInfoThreadLocal.remove(); // 记得清理ThreadLocal
        }
    }

有一点需要注意,因为 ThreadLocal 是线程局部变量,在一个线程里设置的值,只有在本线程内才可以获取,如果进行了线程切换,就无法拿到了。 但是实际应用中,为了提高接口性能,很多时候都会创建新线程进行一些并行处理,这时候如果想要在新线程中也能获取到上下文信息,就要在线程间传递 ContextInfo 了。 为了避免每次都写相同的代码,可以包装一下 Callable 来实现这个目的:

public class CallableWrapper<V> implements Callable<V> {
    private final ContextInfo contextInfo;

    private final Callable<V> task;

    public CallableWrapper(ContextInfo contextInfo, Callable<V> task) {
        this.contextInfo = contextInfo;
        this.task = task;
    }

    @Override
    public V call() throws Exception {
        ContextInfoThreadLocal.set(contextInfo);
        try {
            return task.call();
        } finally {
            ContextInfoThreadLocal.remove();
        }
    }
}

5.参考文章

https://blog.xiaohansong.com/ThreadLocal-memory-leak.html

https://juejin.im/post/5ba9a6665188255c791b0520

http://www.majiang.life/blog/the-smart-way-of-passing-parameter-by-threadlocal/