揭开ThreadLocal的面纱

当初使用C#时,研究过好一阵它的ThreadLocal,以及可以跨线程传递的LogicalCallContext(ExecutionContext),无奈C#不开源(所幸有了.Net Core),只能满世界找文档,找博客。切换到Java后,终于接触到了另一种研究问题的方法:相比于查资料,更可以看代码,调试代码。然后,一切都不那么神秘了。

作用及核心原理

在我看来,Thread Local主要提供两个功能:

  1. 方便传参。提供一个方便的“货架子”,想存就存,想取的时候能取到,不用每层方法调用都传一大堆参数。(我们通常倾向于把公共的数据放到货架子里)
  2. 线程隔离。各个线程的值互不相干,屏蔽了多线程的烦恼。

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. 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应该翻译为【线程本地变量】,意为和普通变量相对。ThreadLocal通常是一个静态变量,但其get()得到的值在各个线程中互不相干。

ThreadLocal的几个核心方法:

  • get() 得到变量的值。如果此ThreadLocal在当前线程中被设置过值,则返回该值;否则,间接地调用initialValue()初始化当前线程中的变量,再返回初始值。
  • set() 设置当前线程中的变量值。
  • protected initialValue() 初始化方法。默认实现是返回null。
  • remove() 删除当前线程中的变量。

原理简述

  • 每个线程都有一个 ThreadLocalMap 类型的 threadLocals 属性,ThreadLocalMap 类相当于一个Map,key 是 ThreadLocal 本身,value 就是我们设置的值。
public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
}
复制代码
  • 当我们通过 threadLocal.set(“猿天地”); 的时候,就是在这个线程中的 threadLocals 属性中放入一个键值对,key 是 当前线程,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);
}
复制代码
  • 当我们通过 threadlocal.get() 方法的时候,就是根据当前线程作为key来获取这个线程设置的值。
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();
}
复制代码

thread-local.png

核心:ThreadLocalMap

ThreadLocalMap is a customized hash map suitable only for maintaining thread local values. To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys. However, since reference queues are not used, stale entries are guaranteed to be removed only when the table starts running out of space.

ThreadLocalMap是一个定制的Hash map,使用开放寻址法解决冲突。

  • 它的Entry是一个WeakReference,准确地说是继承了WeakReference
  • ThreadLocal对象的引用被传到WeakReferencereference中,entry.get()被当作map元素的key,而Entry还多了一个字段value,用来存放ThreadLocal变量实际的值。
  • 由于是弱引用,若ThreadLocal对象不再有普通引用,GC发生时会将ThreadLocal对象清除。而Entry的key,即entry.get()会变为null。然而,GC只会清除被引用对象,Entry还被线程的ThreadLocalMap引用着,因而不会被清除。因而,value对象就不会被清除。除非线程退出,造成该线程的ThreadLocalMap整体释放,否则value的内存就无法释放,内存泄漏
  • JDK的作者自然想到了这一点,因此在ThreadLocalMap的很多方法中,调用expungeStaleEntries()清除entry.get() == null 的元素,将Entry的value释放。
  • 然而,我们大部分的使用场景是,ThreadLocal是一个静态变量,因此永远有普通引用指向每个线程中的ThreadLocalMap的该entry。因此该ThreadLocal的Entry永远不会被释放,自然expungeStaleEntries()就无能为力,value的内存也不会被释放。而在我们确实用完了ThreadLocal后,可以主动调用remove()方法,主动删掉entry。

然而,真的有必要调用remove()方法吗?通常我们的场景是服务端,线程在不断地处理请求,每个请求到来会导致某线程中的Thread Local变量被赋予一个新的值,而原来的值对象自然地就失去了引用,被GC清理。所以不存在泄露

跨线程传递

Thread Local是不能跨线程传递的,线程隔离嘛!但有些场景中我们又想传递。例如:

  1. 启动一个新线程执行某个方法,但希望新线程也能通过Thread Local获取当前线程拥有的上下文(e.g., User ID, Transaction ID)。
  2. 将任务提交给线程池执行时,希望将来执行任务的那个线程也能继承当前线程的Thread Local,从而可以使用当前的上下文。

下面我们就来看一下有哪些方法。

InheritableThreadLocal

原理:InheritableThreadLocal这个类继承了ThreadLocal,重写了3个方法。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    // 可以忽略
    protected T childValue(T parentValue) {
        return parentValue;
    }

    /**
     * Get the map associated with a ThreadLocal.
     *
     * @param t the current thread
     */
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    /**
     * Create the map associated with a ThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the table.
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}
复制代码

可以看到使用InheritableThreadLocal时,map使用了线程的inheritableThreadLocals 字段,而不是之前的threadLocals 字段。

Thread的两个字段及注释

inheritableThreadLocals 字段既然叫可继承的,自然在创建新线程的时候会传递。代码在Thread的init()方法中:

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
复制代码

到此为止,通过inheritableThreadLocals我们可以在父线程创建子线程的时候将ThreadLocal中的值传递给子线程,这个特性已经能够满足大部分的需求了[1]。但是还有一个很严重的问题会出现在线程复用的情况下[2],比如线程池中去使用inheritableThreadLocals 进行传值,因为inheritableThreadLocals 只是会在新创建线程的时候进行传值,线程复用并不会做这个操作。

到这里JDK就无能为力了。C#提供了LogicalCallContext(以及Execution Context机制)来解决,Java要解决这个问题就得自己去扩展线程类,实现这个功能。

阿里开源的transmittable-thread-local

GitHub地址

transmittable-thread-local使用方式分为三种:(装饰器模式哦!)

  1. 修饰Runnable和Callable
  2. 修饰线程池
  3. Java Agent来修饰(运行时修改)JDK线程池实现类。

具体使用方式官方文档非常清楚。

下面简析原理:

  • 既然要解决在使用线程池时的thread local传递问题,就要把任务提交时的当前ThreadLocal值传递到任务执行时的那个线程。
  • 而如何传递,自然是在提交任务前**捕获(capture)当前线程的所有ThreadLocal,存下来,然后在任务真正执行时在目标线程中放出(replay)**之前捕获的ThreadLocal。

代码层面,以修饰Runnable举例:

  1. 创建TtlRunnable()时,一定先调用capture()捕获当前线程中的ThreadLocal
private TtlCallable(@Nonnull Callable<V> callable, boolean releaseTtlValueReferenceAfterCall) {
    this.capturedRef = new AtomicReference<Object>(capture());
    ...
}
复制代码
  1. capture() 方法是Transmitter类的静态方法:
public static Object capture() {
        Map<TransmittableThreadLocal<?>, Object> captured = new HashMap<TransmittableThreadLocal<?>, Object>();
        for (TransmittableThreadLocal<?> threadLocal : holder.get().keySet()) {
            captured.put(threadLocal, threadLocal.copyValue());
        }
        return captured;
}
复制代码
  1. run()中,先放出之前捕获的ThreadLocal。
public void run() {
    Object captured = capturedRef.get();
    ...
    Object backup = replay(captured);
    try {
        runnable.run();
    } finally {
        restore(backup); 
    }
}
复制代码

时序图:

完整时序图

应用

  • Spring MVC的静态类 RequestContextHoldergetRequestAttributes()实际上获得的就是InheritableThreadLocal<RequestAttributes>在当前线程中的值。也可以说明它可以传递到自身创建的线程中,但对已有的线程无能为力。

    至于它是什么什么被设置的,可以参考其注释:Holder class to expose the web request in the form of a thread-bound RequestAttributes object. The request will be inherited by any child threads spawned by the current thread if the inheritable flag is set to true. Use RequestContextListener or org.springframework.web.filter.RequestContextFilter to expose the current web request. Note that org.springframework.web.servlet.DispatcherServlet already exposes the current request by default.

  • Spring

  • 阿里巴巴TTL总结的几个应用场景

...

一些坑

(未完待续,坑挖待填)

猜你喜欢

转载自juejin.im/post/5ca0238ae51d453d6970e1f2