Java怎么实现父子线程的值传递?InheritableThreadLocal类和transmittable-thread-local类?

前言:在Java中使用ThreadLocal类时,怎么实现父子线程直接的值传递呢?

假设这样使用会有问题吗?

public class Main {
    private static final ThreadLocal threadlocal =  new ThreadLocal<>();

    public static void main(String[] args) {
           threadlocal.set("父线程设置");
        new Thread( () -> {
                System.out.println("子线程读取" + threadlocal.get());
        }).start();
        System.out.println();
    }
}

如果直接这样写的话是获取不到父线程的值的。

Java怎么实现父子线程的值传递?InheritableThreadLocal类和transmittable-thread-local类?_第1张图片

怎么解决呢

这里介绍俩种方式;

InheritableThreadLocal类

使用jdk自带的,InheritableThreadLocal

public class Main {
    // 使用InheritableThreadLocal
    private static final InheritableThreadLocal threadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) {
           threadLocal.set("父线程设置");
        new Thread( () -> {
                System.out.println("子线程读取\t" + threadLocal.get());
        }).start();
        System.out.println();
    }
}

使用这个类实现threadlocal后可以发现可以读取到了;

Java怎么实现父子线程的值传递?InheritableThreadLocal类和transmittable-thread-local类?_第2张图片

那么是怎么实现的呢?

 主要就是这俩个机制:线程创建时的值拷贝机制 和 Thread类的特殊设计

进入源码查看,

首先:InheritableThreadLocal集成了ThreadLocal类。
public class InheritableThreadLocal extends ThreadLocal {
  
    public InheritableThreadLocal() {}

    // 重写childValue方法(可自定义继承逻辑)
    protected T childValue(T parentValue) {
        return parentValue;
    }

    // // 关键方法:获取线程的inheritableThreadLocals变量
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

  
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

这是Thread类的一些片段,截取出来了,主要看注释的地方、在init()方法中


class Thread {
    // 普通ThreadLocal存储
    ThreadLocal.ThreadLocalMap threadLocals;
    
    // 专门用于继承的ThreadLocal存储
    ThreadLocal.ThreadLocalMap inheritableThreadLocals;
    
    // 线程初始化方法
    private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
        init(g, target, name, stackSize, null, true);
    }
    
//在init方法中,使用if判断了一下父线程是不是null的,就是检查父线程的
//inheritableThreadLocals,如果存在父线程则会触发拷贝的逻辑。
//会调用createInheritedMap这个方法,子线程就可以获得一个新的ThreadLoaclMap对象。
//这样就完成了拷贝的过程
//if (inheritThreadLocals && parent.inheritableThreadLocals != null) {
            // 关键点:创建时拷贝父线程的inheritableThreadLocals
            //this.inheritableThreadLocals = 
               // ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
//}
    private void init(..., boolean inheritThreadLocals) {
        Thread parent = currentThread();
        if (inheritThreadLocals && parent.inheritableThreadLocals != null) {
            // 关键点:创建时拷贝父线程的inheritableThreadLocals
            this.inheritableThreadLocals = 
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        }
        // ...其他初始化
    }
}

createInheritedMap源码解读

//调用createInheritedMap时,会将父线程的ThreadLocalMap对象传入过去
//此时就会遍历map对象逐个复制entry到当前线程的ThreadLocalMap中去。
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    // 创建新Map并逐个复制Entry
//创建了一个全新的ThreadLocalMap对象,而不是简单的引用传递
    ThreadLocalMap map = new ThreadLocalMap();
    for (Entry e : parentMap.getTable()) {
        if (e != null) {
            @SuppressWarnings("unchecked")
            ThreadLocal key = (ThreadLocal) e.get();
            if (key != null) {
                // 调用childValue方法处理继承值!
                //这里我们可以重新childValue方法实现修改父线程的值。
/*
​​​​​​​InheritableThreadLocal itl = new InheritableThreadLocal() {
    @Override
    protected String childValue(String parentValue) {
        return "Child_" + parentValue; // 可修改继承的值
    }
};

*/
                Object value = key.childValue(e.value);
                map.set(key, value);
            }
        }
    }
    return map;
}
//此后子线程读父线程的值的时候,就可以读取到拷贝的值了。 
  

以上就是全部的过程了。这样就通过继承的方式实现了一个可传递父子线程值的类,

同时,我们根据上面的过程分析,可以发现还是有很多不足的;

其一因为是通过for循环遍历方式来拷贝的,如果父线程的InheritableThreadLocal中有大量数据,会降低线程创建速度。

其二有内存泄漏风险,和ThreadLocal一样需要手动remove(),否则可能导致:子线程长期持有父线程的引用,大对象无法被GC。

其三:最重要的是在线程池环境下,因为线程的复用,而不是new Thread()这样的方式创建,从而不会触发init()方法,但是拷贝的过程在init()方法中,所以在线程复用的情况下会失效。

TransmittableThreadLocal

TransmittableThreadLocal (TTL) 是阿里巴巴开源的一个线程本地变量解决方案,它解决了 InheritableThreadLocal 在线程池环境下无法正确传递值的问题。

初始化过程,

public static final TransmittableThreadLocal ttl = new TransmittableThreadLocal<>();

//还需要使用TtlExecutors.getTtlExecutorService()对我们的线程池做了增强(这也是必须的搭配,否则
//没法使用 TransmittableThreadLocal 特性)
 private static ExecutorService businessExecutors = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(5));

 可以点进去看具体的源码,可以看见具体的结构

//可以看见继承了java.lang.InheritableThreadLocal 
//还实现了com.alibaba.ttl.TtlCopier这个类
public class TransmittableThreadLocal  
extends java.lang.InheritableThreadLocal 
implements com.alibaba.ttl.TtlCopier {
 /***/

}


package com.alibaba.ttl;
//可以看见TtlCopier是一个函数式接口
@java.lang.FunctionalInterface
public interface TtlCopier  {
    T copy(T t);
}

具体是怎么实现的呢?

分为4步过程:

第一步:父线程设置值(ThreadLocal.set)

TransmittableThreadLocal context = new TransmittableThreadLocal<>();
context.set("parent-value"); // 设置父线程的值

他的存储结构是什么样的呢?

//维护了一个 全局 holder(WeakHashMap)来跟踪所有 TTL实例:

static ThreadLocal, ?>> holder = 
    new ThreadLocal, ?>>() {
        @Override
        protected Map, ?> initialValue() {
            return new WeakHashMap<>(); // 弱引用防止内存泄漏
        }
    };

//调用set时,重写了InheritableThreadLocal的set方法,
//这里的disableIgnoreNullValueSemantics && value == null语句中
//disableIgnoreNullValueSemantics指的是  是否禁用空值语义 。控制 null 值的存储行为
//如果允许的话,value == null 就 不走remove方法,如果不允许并且value == null就会走remove方法
@Override
    public final void set(T value) {
        if (!disableIgnoreNullValueSemantics && value == null) {
            // may set null to remove value
            remove();
        } else {
            super.set(value);
            addThisToHolder();
        }
    }

第二步:提交任务时获取值


//当向线程池提交任务时,TTL 会 捕获当前线程的所有 TTL 值:
ExecutorService executor = Executors.newFixedThreadPool(1);
executor.execute(TtlRunnable.get(() -> {
    // 子线程逻辑
}));

//关键方法 TtlRunnable.get():
public static Runnable get(Runnable runnable) {
    // 1. 捕获当前线程的所有 TTL 值(快照)
    Map, Object> captured = capture();
    return new TtlRunnable(runnable, captured);
}

//capture() 源码:此时,captured 是一个 键值对快照,保存了所有 TTL 的当前值。
static Map, Object> capture() {
    Map, Object> captured = new HashMap<>();
    // 遍历 holder 中所有 TTL 实例,拷贝它们的值
    for (TransmittableThreadLocal threadLocal : holder.get().keySet()) {
        captured.put(threadLocal, threadLocal.copyValue()); // 深拷贝值
    }
    return captured;
} 
  

第三步:子线程执行前恢复值(replay)


//当线程池中的线程执行任务时,TTL 会 将捕获的值恢复到当前线程:
public void run() {
    Map, Object> backup = replay(captured);
    try {
        runnable.run(); // 执行用户任务
    } finally {
        restore(backup); // 恢复线程原始状态
    }
}
//为什么需要replay呢
//因为在子线程中可能会修改ThreadLocal的值,另外restore里面会主动调用remove()回收,
//避免内存泄露(会删除子线程新增的TTL)
//有下列两种情况:
//1. 一种情况是:主线程启动了一个异步任务,此时主线程和子线程会并行,
//由于父子线程的数据是隔离开的,子线程此时对TTL中的内容进行修改并不会影响到原线程的逻辑
//2. 另一种情况是:线程池的拒绝策略为CallerRunsPolicy时,
//那么在主线程内启动这个异步任务可能会有当前的主线程来执行,
//那么线程之间的数据并不会隔离,那么如果对ThreadLocal中的数据进行了修改,
//那么将会影响到程序的正常运行。
//replay() 源码:

//先备份当前线程的原始值(防止污染)。

//清除当前线程的 TTL 值(避免旧值干扰)。

//将父线程的快照值设置到当前线程。

static Map, Object> replay(
    Map, Object> captured) {
    
    // 1. 备份当前线程的原始 TTL 值
    Map, Object> backup = new HashMap<>();
    for (TransmittableThreadLocal threadLocal : holder.get().keySet()) {
        backup.put(threadLocal, threadLocal.get());
    }

    // 2. 清除当前线程的所有 TTL 值
    for (TransmittableThreadLocal threadLocal : captured.keySet()) {
        threadLocal.superRemove(); // 调用 InheritableThreadLocal.remove
    }

    // 3. 将捕获的父线程值设置到当前线程
    for (Map.Entry, Object> entry : captured.entrySet()) {
        entry.getKey().set(entry.getValue()); // 调用 TTL.set
    }

    return backup; // 返回备份的原始值
} 
  

第四步:子线程读取值(TTL.get)

//在任务执行期间,子线程通过 TTL.get() 读取到的值 来自父线程的快照:
executor.execute(TtlRunnable.get(() -> {
    System.out.println(context.get()); // 输出 "parent-value"
}));

//get方法源码,重写了父类的get方法,
//由于之前通过 replay() 设置了值,这里会正确返回父线程传递的值
@Override
    public final T get() {
        T value = super.get();
//判断是否允许null的存在,默认是false的。前面介绍果。
        if (disableIgnoreNullValueSemantics || value != null) addThisToHolder();
        return value;
    }

第五步:任务执行后清理(restore)

//任务执行完成后,TTL 会 恢复线程的原始状态,避免影响后续任务:
//确保线程池中的线程在执行完任务后,不会残留本次任务的 TTL 值,避免内存泄漏和值污染。
static void restore(Map, Object> backup) {
    // 1. 清除当前线程的所有 TTL 值
    for (TransmittableThreadLocal threadLocal : holder.get().keySet()) {
        threadLocal.superRemove();
    }

    // 2. 还原备份的原始值
    for (Map.Entry, Object> entry : backup.entrySet()) {
        entry.getKey().set(entry.getValue());
    }
} 
  

以上就是实现的全部过程了,其中他还包装一些类,

包装机制和值快照技术,完美解决了线程池环境下的上下文传递问题,是Java异步编程中不可或缺的工具。

// 包装普通Runnable
public static Runnable wrap(Runnable runnable) {
    Map, Object> captured = capture();
    return () -> {
        Map, Object> backup = replay(captured);
        try {
            runnable.run();
        } finally {
            restore(backup);
        }
    };
}

在capture/replay这俩个方法中有一定开销,因为是for循环遍历来操作数据的。我们需要避免存储大对象。

并且TTL是存在线程安全问题的,因为默认都是引用类型拷贝,如果子线程修改了数据,主线程是可以感知到的。

你可能感兴趣的:(java,jvm,开发语言)