ThreadLocal 是 Java 实现线程私有存储的一种方式,通过 ThreadLocal 可以在当前线程下线程安全的读取、修改全局的变量。
线程私有虽然保障了线程安全,但是却在某些场景下带来了麻烦,常见的在 Web 应用中利用 ThreadLocal 作为用户上下文的实现,在应用中全局传递用户的信息,如果在业务流程中存在异步执行的情况,那么在异步执行的过程中,将无法通过上下文拿不到用户信息。
想要解决上面提到的问题,就需要实现 ThreadLocal 的跨线程传递。

InheritableThreadLocal

通过 InheritableThreadLocal 可以在父子线程之间传递 ThreadLocal ,父线程为当前线程,而子线程即为在当前线程中声明的线程。如果 ThreadLocal 为 InheritableThreadLocal ,子线程就可以拿到父线程在 ThreadLocal 中设置的值。

// 在父线程中设置
InheritableThreadLocal<String> parent = new InheritableThreadLocal<String>();
parent.set("value-set-in-parent");
 
// =====================================================
 
// 在子线程中可以读取,值是"value-set-in-parent"
String value = parent.get();

ThreadLocal 的实现原理是在每个 Thread 实例中,都维护了一个 ThreadLocalMap 对象,而 ThreadLocal 为 key 在线程中存储值。而每个 Thread 中,其实都存储有两个 ThreadLocalMap 。

ThreadLocal.ThreadLocalMap threadLocals = null;
 
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

而 InheritableThreadLocal 只是简单的重写了几个方法:

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
 
    protected T childValue(T parentValue) {
        return parentValue;
    }
 
    /**
     * 获取的是线程的 inheritableThreadLocals ,以 inheritableThreadLocals 作为线程私有存储的存储结构
     *
     * @param t 当前线程
     */
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
 
    /**
     * 创建 ThreadLocalMap 并赋值到线程的 inheritableThreadLocals ,以 inheritableThreadLocals 作为线程私有存储的存储结构
     *
     * @param t 当前线程
     * @param firstValue 第一个加入到 ThreadLocalMap 的值
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

而在 Thread 的 init 方法中将会传递 InheritableThreadLocal 中的数据。

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    }
    this.name = name;
    Thread parent = currentThread();
    // 省略代码
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    // 省略代码
}

在使用线程池的情况下传递 ThreadLocal

虽然 InheritableThreadLocal 可以做到 ThreadLocal 的传递,但是仅限于父子线程,而大多的业务场景都是使用线程池的,InheritableThreadLocal 将难以起到作用。
在这种情况下为了传递 ThreadLocal 中的值,可能需要作出一些妥协,例如将 ThreadLocal 中需要的值取出,显式传递到任务中,显然这样是不优雅的,会让方法增加了一些与原本业务无关的参数。

而对于上述提到的情况,可以尝试使用阿里的 Transmittable ThreadLocal(TTL) 。TTL 的很简单,文档已有详细的描述,这里不过多叙述。这里简单的分析一下 TTL 的实现,以此作为 ThreadLocal 在线程池场景下传递实现的参考。

根据文档 TTL 在使用时使用类 TransmittableThreadLocal 替代 ThreadLocal ,且需要将相关的 Runnable 或是 Callable 等类进行包装。

TransmittableThreadLocal 继承了 InheritableThreadLocal ,内部维护了一个静态的 InheritableThreadLocal ,存储的是而 TransmittableThreadLocal 作为 key 的 WeakHashMap 。

private static InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>> holder =
        new InheritableThreadLocal<Map<TransmittableThreadLocal<?>, ?>>() {
            @Override
            protected Map<TransmittableThreadLocal<?>, ?> initialValue() {
                return new WeakHashMap<>();
            }
 
            @Override
            protected Map<TransmittableThreadLocal<?>, ?> childValue(Map<TransmittableThreadLocal<?>, ?> parentValue) {
                return new WeakHashMap<TransmittableThreadLocal<?>, Object>(parentValue);
            }
        };

holder 会将应用中用到的 TransmittableThreadLocal 作为 key 存储起来。调用 TransmittableThreadLocal 的 get/set 方法都会调用一个 addValue 方法,而就是在这个 addValue 方法中完成存储的过程。

private void addValue() {
    if (!holder.get().containsKey(this)) {
        holder.get().put(this, null); // WeakHashMap supports null value.
    }
}

之后想要线程私有存储实现跨线程传递,需要将 Runnable 或 Callable 包装成 TtlRunnable 、TtlCallable 。这里以 TtlRunnable 为例,在包装时,将会调用 TtlRunnable 的构造方法。

private TtlRunnable(@Nonnull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
    this.capturedRef = new AtomicReference<Object>(capture());
    this.runnable = runnable;
    this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
}

其中 capture 方法就是获取一份当前线程使用 TransmittableThreadLocal 存储起来的私有存储数据的拷贝。

@Nonnull
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;
}

而这份拷贝在实际的 Runnable 运行前,会设置到当前所使用的线程池的线程的存储空间中,在后续的执行过程中,可以通过相同的 TransmittableThreadLocal 实例获取到之前线程所存储的数据。
而且由于线程池的线程是会复用的,所以在设置之前线程的私有存储前,会将该线程池线程的私有存储备份,在任务执行完成后将会复原数据。

@Override
public void run() {
    Object captured = capturedRef.get();
    if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
        throw new IllegalStateException("TTL value reference is released after run!");
    }
 
    Object backup = replay(captured);
    try {
        runnable.run();
    } finally {
        restore(backup);
    }
}

总结

Java 的私有存储机制是将值存储在线程的私有空间中,而 ThreadLocal 是拿到这个存储在私有空间的值的索引。当出现跨线程的状况时,如果想实现跨线程传递私有存储的值,就需要将这个值转存到公有的空间中(通常是类的成员域),之后再从公有空间中复制到另外一个线程的私有空间中,最后还要可以用同一个 ThreadLocal ,换句话来说可以用相同的索引找到相同的值。而 TTL 就是做了上述的事情(不仅仅)实现跨线程的 ThreadLocal 传递。

ThreadLocal 跨线程传递_子线程