写点什么

从 TL、ITL 到 TTL | 京东物流技术团队

  • 2023-07-17
    北京
  • 本文字数:6631 字

    阅读完需:约 22 分钟

从TL、ITL到TTL | 京东物流技术团队

1、概述

ThreadLocal(TL)是 Java 中一种线程局部变量实现机制,他为每个线程提供一个单独的变量副本,保证多线程场景下,变量的线程安全。经常用于代替参数的显式传递。


InheritableThreadLocal(ITL)是 JDK 提供的 TL 增强版,而 TransmittableThreadLocal(TTL)是阿里开源的 ITL 增强版


这些 ThreadLocal 在不同场景下有不同用途,我们来分析一下:

2、ThreadLocal

ThreadLocal 主要的方法有四个:initialValue、set、get、remove

2.1、初始化——initialValule

当线程首次访问该 ThreadLocal 时(ThreadLocal.get()),会进行初始化赋值。我们常用两种方法初始化 ThreadLocal

2.1.1、重写 initialValue

ThreadLocal<String> threadLocal = new ThreadLocal<String>() {    @Override    protected String initialValue() {        return "";    }};
复制代码

2.1.2、调用 ThreadLocal.withInitial

ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "");
复制代码


他会创建一个 SuppliedThreadLocal 内部类


public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {    return new SuppliedThreadLocal<>(supplier);}
复制代码


该类重写了 initialValue 方法


static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;
SuppliedThreadLocal(Supplier<? extends T> supplier) { this.supplier = Objects.requireNonNull(supplier); }
@Override protected T initialValue() { //当该线程首次访问ThreadLocal时,会间接调用lambda表达式初始化 return supplier.get(); }}
复制代码


⚠️ITL 并没有重新实现 withInitial,如果使用 withInitial 则会创建 STL,失去自己增强的特性

2.2、赋值——set

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,类定义在 ThreadLocal 中,是 Thread 的成员变量


ThreadLocalMap getMap(Thread t) {    return t.threadLocals;}
复制代码


ThreadLocalMap 内部还有一个内部类 Entry,是存值的地方


static class ThreadLocalMap {    static class Entry extends WeakReference<ThreadLocal<?>> {        Object value;        Entry(ThreadLocal<?> k, Object v) {            //ThreadLocal的引用是“key”            super(k);            //线程局部变量是value            value = v;        }    }    //Entry数组    //value具体放在哪个index下,是由ThreadLocal的hashCode算出来的    private Entry[] table;}
复制代码

2.3、取值——get

public T get() {    Thread t = Thread.currentThread();    //1、获取线程的ThreadLocalMap    ThreadLocalMap map = getMap(t);    if (map != null) {        //2、根据ThreadLocal的hashCode,获取对应Entry下的value        ThreadLocalMap.Entry e = map.getEntry(this);        if (e != null) {            @SuppressWarnings("unchecked")            T result = (T)e.value;            return result;        }    }    //3、如果没有赋过值,则初始化    return setInitialValue();}
复制代码

2.4、清空——remove

 public void remove() {     ThreadLocalMap m = getMap(Thread.currentThread());     if (m != null)         //会将对应Entry、包括他的key、value手动置null         m.remove(this); }
复制代码

3、InheritableThreadLocal

3.1、TL 在父子线程场景下存在的问题

我们先来看一个例子


public static void main(String[] args) throws InterruptedException {    ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> "A");    threadLocal.set("B");    Thread thread = new Thread(() -> {        System.out.println("子线程ThreadLocal:" + threadLocal.get());    }, "子线程");    thread.start();    thread.join();}
复制代码


打印结果如下,可见子线程的 ThreadLocal 是初始值,并没有使用父线程修改后的值:


子线程ThreadLocal:A
复制代码


线程的 ThreadLocalMap 是首次访问时创建的,所以子线程使用 ThreadLocal 的时候,会初始化一个新的 ThreadLocal,线程局部变量为默认值


⚠️所以,TL 不具有遗传性

3.2、ITL 的解决方案

为了解决 TL 子线程遗传性的问题,JDK 引入了 ITL


他继承 ThreadLocal,重写了 childValue、getMap、createMap 三个方法


public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) { return parentValue; }
ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; }
void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); }}
复制代码


这里出现了 inheritableThreadLocals,他存储的就是从父线程拷贝过来的 ThreadLocal,这个值是在父线程首次修改 ThreadLocal 的时候赋值的,然后在子线程创建时拷贝过来的


//父线程部分:public void set(T value) {    Thread t = Thread.currentThread();    //该方法被ITL重写,访问inheritableThreadLocals为null    ThreadLocalMap map = getMap(t);    if (map != null)        map.set(this, value);    else    //该方法同样被ITL重写,创建一个ThreadLocalMap赋值给inheritableThreadLocals        createMap(t, value);}
//子线程部分:public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0);}
private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { //省略一些代码...
//获取当前线程(父线程、也就是创建子线程的线程) Thread parent = currentThread(); //1、允许ThreadLocal遗传(这个默认为true) //2、inheritableThreadLocals不为空,因为父线程调用set了 //父线程不调用set,那ThreadLocal就是初始值,那直接初始化就好了,也不用进该分支 if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);}
//createInheritedMap使用该构造函数,根据父线程的inheritableThreadLocals进行深拷贝private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); table = new Entry[len]; //深拷贝父线程ThreadLocalMap for (int j = 0; j < len; j++) { Entry e = parentTable[j]; if (e != null) { @SuppressWarnings("unchecked") ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); if (key != null) { //childValue被ITL重写,返回父线程ThreadLocal的值 Object value = key.childValue(e.value); Entry c = new Entry(key, value); int h = key.threadLocalHashCode & (len - 1); while (table[h] != null) h = nextIndex(h, len); table[h] = c; size++; } } }}
复制代码


使用 ITL 的效果


public static void main(String[] args) throws InterruptedException {        ThreadLocal<String> threadLocal = new InheritableThreadLocal<String>() {            @Override            protected String initialValue() {                return "A";            }        };        threadLocal.set("B");        Thread thread = new Thread(() -> {            System.out.println("子线程ThreadLocal:" + threadLocal.get());        }, "子线程");        thread.start();
thread.join();}
复制代码


打印结果如下,子线程拷贝了父线程 ThreadLocal:


子线程ThreadLocal:B
复制代码


总结一下,ITL 解决父子线程遗传性的核心思路是,将可遗传的 ThreadLocal 放在父线程新的 ThreadLocalMap 中,在子线程首次使用时进行拷贝

4.、TransmittableThreadLocal

4.1、ITL 在线程复用场景下存在的问题

我们再从一个简单的例子说起


public static void main(String[] args) throws InterruptedException, ExecutionException {    ThreadLocal<String> threadLocal = new InheritableThreadLocal<String>() {        @Override        protected String initialValue() {            return "A";        }    };    threadLocal.set("B");    ExecutorService executorService = Executors.newFixedThreadPool(1);    //1、子线程第一次获取ThreadLocal    executorService.submit(() -> System.out.println("子线程ThreadLocal:"+threadLocal.get())).get();    Thread.sleep(1000);    //2、父线程修改ThreadLocal    threadLocal.set("C");    System.out.println("父线程修改ThreadLocal为"+threadLocal.get());    //3、子线程第二次获取ThreadLocal    executorService.submit(() -> System.out.println("子线程ThreadLocal:"+threadLocal.get())).get();}
复制代码


打印结果如下,子线程在第二次打印时,并没有拷贝父线程的 ThreadLocal,使用的还是首次拷贝的值:


子线程ThreadLocal:B父线程修改ThreadLocal为C子线程ThreadLocal:B
复制代码


⚠️可复用的子线程不会感知父线程 ThreadLocal 的变化

4.2、TTL 的解决方案

4.2.1、TTL 的使用

TTL 在 ITL 上做了稍微复杂的封装,我们从使用开始了解


引入依赖


<dependency>    <groupId>com.alibaba</groupId>    <artifactId>transmittable-thread-local</artifactId>    <version>latest</version></dependency>
复制代码


在使用 TTL 时,线程需要经过 TTL 封装,线程池同理


public static void main(String[] args) throws InterruptedException, ExecutionException {    ThreadLocal<String> threadLocal = new TransmittableThreadLocal<String>() {        @Override        protected String initialValue() {            return "A";        }    };    threadLocal.set("B");    ExecutorService executorService = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));    executorService.submit(() -> System.out.println("子线程ThreadLocal:" + threadLocal.get())).get();    Thread.sleep(1000);    threadLocal.set("C");    System.out.println("父线程修改ThreadLocal为" + threadLocal.get());    executorService.submit(() -> System.out.println("子线程ThreadLocal:" + threadLocal.get())).get();    Thread.sleep(1000);    executorService.submit(() -> {        threadLocal.set("D");        System.out.println("子线程修改ThreadLocal为" + threadLocal.get());    });    Thread.sleep(1000);    executorService.submit(() -> System.out.println("子线程ThreadLocal:" + threadLocal.get()));    Thread.sleep(1000);}
复制代码


打印结果如下,子线程每次都会获取父线程的 ThreadLocal


子线程ThreadLocal:B父线程修改ThreadLocal为C子线程ThreadLocal:C子线程修改ThreadLocal为D子线程ThreadLocal:C
复制代码


从使用上看,TTL 要求将任务封装,那我们就从 ThreadLocal 和 ExecutorService 两部分入手

4.2.2、TTL 对 ThreadLocal 的封装

下面是 TTL 的取值和赋值逻辑,都涉及一个关键方法 addThisToHolder,对应的属性 holder 会在线程池执行任务时用到


//TransmittableThreadLocal.addThisToHolder()private void addThisToHolder() {    //InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder    if (!holder.get().containsKey(this)) {        //holder是静态变量,他会把TTL存到当前线程的map中        //value是null,他其实是把Map当Set用        //主线程赋值时,会获取主线程的holderMap,然后把TTL存进去        holder.get().put((TransmittableThreadLocal<Object>) this, null);    }}
@Overridepublic final void set(T value) { if (!disableIgnoreNullValueSemantics && null == value) { remove(); } else { super.set(value); //当主线程赋值时,会将自己的TTL放到自己的map中 addThisToHolder(); }}
@Overridepublic final T get() { T value = super.get(); if (disableIgnoreNullValueSemantics || null != value) addThisToHolder(); return value;}
复制代码

4.2.3、TTL 对任务的封装

//我们通过TtlExecutors.getTtlExecutorService()对线程池进行封装public static ExecutorService getTtlExecutorService(@Nullable ExecutorService executorService) {    if (TtlAgent.isTtlAgentLoaded() || executorService == null || executorService instanceof TtlEnhanced) {        return executorService;    }    //入参是线程池,通过包装类代理线程池的操作    return new ExecutorServiceTtlWrapper(executorService);}
//ExecutorServiceTtlWrapper.submit()public Future<?> submit(@NonNull Runnable task) { //将提交的任务进行封装 return executorService.submit(TtlRunnable.get(task));}
复制代码

4.2.3.1、任务构建

TtlRunnable 构造方法


这里都是主线程在操作,因为任务是主线程提交的


private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {    this.capturedRef = new AtomicReference<Object>(capture());    this.runnable = runnable;    this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;}
复制代码


这里有一个关键属性 capturedRef,他是一个原子引用,存了 TTL


//TrasmitableThreadLocal.Transmitterpublic static Object capture() {    //获取ttl的值构建快照    return new Snapshot(captureTtlValues(), captureThreadLocalValues());}
private static HashMap<TransmittableThreadLocal<Object>, Object> captureTtlValues() { HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value = new HashMap<TransmittableThreadLocal<Object>, Object>(); for (TransmittableThreadLocal<Object> threadLocal : holder.get().keySet()) { //将主线程TTL的值存到当前任务中 ttl2Value.put(threadLocal, threadLocal.copyValue()); } return ttl2Value;}
复制代码

4.2.3.2、任务执行

任务执行的代码如下,在任务执行前回放 ThreadLocal,在任务执行后恢复 ThreadLocal:


这里都是子线程在操作,因为任务都是子线程执行的


@Overridepublic void run() {    Object captured = capturedRef.get();    if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {        throw new IllegalStateException("TTL value reference is released after run!");    }    //1、备份子线程ThreadLocal    //2、使用主线程提交任务时构建的ThreadLocal副本,将子线程ThreadLocal覆盖    Object backup = replay(captured);    try {        //3、任务执行        runnable.run();    } finally {        //3、使用之前备份的子线程ThreadLocal进行恢复        restore(backup);    }}
复制代码


总结一下,TTL 让子线程感知父线程变化的核心思路是,主线程在任务提交时构建 ThreadLocal 副本,在子线程执行任务时供其使用


⚠️提交和执行任务会对 TTL 进行若干操作,理论上对性能有一点点影响,官方性能测试结论说损耗可忽略


TTL官方性能测试


作者:京东物流 刘朝永

来源:京东云开发者 自猿其说

发布于: 刚刚阅读数: 3
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
从TL、ITL到TTL | 京东物流技术团队_ThreadLocal_京东科技开发者_InfoQ写作社区