父子线程间参数传递方案

一、需求及方案

需求:

        1. 公参传递处理解决方案

        2. 全链路压测标解决方案

         接口公共参数可以认为是每次调用都需要带的用户信息/地址/版本/设备/网络/链路压测标

方案:

        1:使用threadLocal

        2:使用TransmittableThreadLocal

二、原理:理解下TL、ITL、TTL分别解决的事什么问题,如何解决的

结论:

        1. ThreadLocal解决了参数在一个线程中各个函数之间互相传递的问题(Thread类的成员变量 ThreadLocal.ThreadLocalMap threadLocals)

        2. InheritableThreadLocal解决的是父子线程之间参数传递的问题(此类扩展ThreadLocal以提供从父线程到子线程的值继承:当创建子线程时,子线程接收父线程具有值的所有可继承线程局部变量的初始值,但是在实际使用的时候,父线程 和 子线程 我们都会使用线程池来进行管理)

        3.TransmittableThreadLocal 解决的是池化情况下父子线程间参数传递的问题(利用 设计模式中的 装饰者模式 对 线程中的 executor/executorService、runnable、Callable 进行封装从而达到父线程向线程池提交任务时将父线程中的  inheritableThreadLocals 继承给 要运行的子线程(无论该线程是从线程池复用的还是新创建的)

方案一:ThreadLocal

        希望在父线程定义一个threadLocal对象,父线程创建的所有子线程都可以使用父线程 threadLocal 中设置的 value 变量(可以是引用对象)。解决办法:在父线程中定义 value 对应的全局引用变量,定义全局线程本地变量,全局线程本地变量中 通过 set() 方法设置了  value 值(全局引用变量);每个子线程在运行的时候都需要存在一段向 全局线程本地变量 threadLocal 设置value 值(全局引用变量)值的 逻辑 :手动为每个线程设置tl的值,再手动remove该entry,每开启一个线程,这两步都必不可少,避免出现内存泄漏。

示例代码:

public class Service {
    private static ThreadLocal<Integer> requestIdThreadLocal = new ThreadLocal<>();
    public static void main(String[] args) {
        Integer reqId = new Integer(5);
        Service a = new Service();
        a.setRequestId(reqId);
    }

    public void setRequestId(Integer requestId) {
        requestIdThreadLocal.set(requestId);
        doBussiness();
    }

    public void doBussiness() {
        System.out.println("首先打印requestId:" + requestIdThreadLocal.get());
        (new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子线程启动");
                System.out.println("在子线程中访问requestId:" + requestIdThreadLocal.get());
            }
        })).start();
    }
}

数据结构

1) 每一个threadLocal 线程本地变量 都会在被使用的线程的 threadLocals 属性 中,threadLocals 属性 是 ThreadLocalMap类型,该类型中 有 table属性,用来 专门存储 线程本地变量;

  2) table属性 是一个数组,数组中的每一项 形似 key-value ,key为 threadLocal 线程本地变量, 值就是该threadLocal对象存放的 value 变量。

从以上分析无论是 get 还是 set 方法 都是从当前线程中获取 threadLocals 对象,然后从 table 属性中设置/获取 threadLocal对象 对应的 value 变量。

为什么会内存泄漏:

        Entry 对象的 key 即 ThreadLocal 类是继承于 WeakReference 弱引用类。具有弱引用的对象有更短暂的生命周期,在发生 GC 活动时,无论内存空间是否足够,垃圾回收器都会回收具有弱引用的对象。

       由于 Entry 对象的 key 是继承于 WeakReference 弱引用类的,若 ThreadLocal 类没有外部强引用,当发生 GC 活动时就会将 ThreadLocal 对象回收。

        而此时如果创建 ThreadLocal 类的线程依然活动,那么 Entry 对象中 ThreadLocal 对象对应的 value 就依旧具有强引用而不会被回收,从而导致内存泄漏。

        在线程池化的情况下,线程不结束归还到线程池中,那么就会导致内存泄漏

类结构

流程

缺点:

1) 该方式过于繁琐,子线程运行的时候,都要在其中给threadLcoal 设置值;

2) 若值是父线程动态生成的或一直在变化的,子线程在运行过程中无法获取到;

引申解决办法:

    直接使用 InheriableThreadLocal

方案二 TransmittableThreadLocal

   先了解下ITL(InheritableThreadLocal)。ITL解决的是父子线程之间参数传递的问题(此类扩展TL以提供从父线程到子线程的值继承:当创建子线程时,子线程接收父线程具有值的所有可继承线程局部变量的初始值

示例代码:

public class Service {
    private static InheritableThreadLocal<Integer> requestIdThreadLocal = new InheritableThreadLocal<>();
    public static void main(String[] args) {
        Integer reqId = new Integer(5);
        Service a = new Service();
        a.setRequestId(reqId);
    }

    public void setRequestId(Integer requestId) {
        requestIdThreadLocal.set(requestId);
        doBussiness();
    }

    public void doBussiness() {
        System.out.println("首先打印requestId:" + requestIdThreadLocal.get());
        (new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("子线程启动");
                System.out.println("在子线程中访问requestId:" + requestIdThreadLocal.get());
            }
        })).start();
    }
}

类结构

        在父线程中定义全局线程本地变量为 InheritableThreadLocal 类型,父线程启动的时候,通过 InheritableThreadLocal 的 set 方法设置 value变量,该 线程本地变量(InheritableThreadLocal)会被放在父线程的 inheriableThreadLocals 的 table属性中,其中 key 为 InheritableThreadLocal对象, value 为 设置的 value变量。

        父线程创建子线程的时候 会自动继承 父线程中的 inheriableThreadLocals ,放在子线程的 inheriableThreadLocals (它是一个ThreadLocalMap 类型 )中,在Thread类的构造函数中init方法初始化inheriableThreadLocals 这个变量

缺点:

        复制的时机为创建子线程时,但若用线程池时使用InheritableThreadLocal,那么线程池中的线程拷贝的数据来自于第一个提交任务的外部线程,即后面的外部线程向线程池中提交任务时,子线程访问的本地变量都来源于第一个外部线程,造成线程本地变量混乱

错乱示例:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author by lianyingying
 * @Classname Service
 * @Description TODO
 * @Date 2021/10/26 10:04 上午
 * @since JDK 1.8
 * Copyright (c) 2021 imdada System Incorporated All Rights Reserved.
 */
public class Service {
    /**
     * 模拟tomcat线程池
     */
    private static ExecutorService tomcatExecutors = Executors.newFixedThreadPool(10);
    /**
     * 业务线程池,默认Control中异步任务执行线程池
     */
    private static ExecutorService businessExecutors = Executors.newFixedThreadPool(5);
    /**
     * 线程上下文环境,模拟在Control这一层,设置环境变量,然后在这里提交一个异步任务,模拟在子线程中,是否可以访问到刚设置的环境变量值。
     */
    private static InheritableThreadLocal<Integer> requestIdThreadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) {

        for(int i = 0; i < 10; i ++ ) {  // 模式10个请求,每个请求执行ControlThread的逻辑,其具体实现就是,先输出父线程的名称,
            //  然后设置本地环境变量,并将父线程名称传入到子线程中,在子线程中尝试获取在父线程中的设置的环境变量
            tomcatExecutors.submit(new ControlThread(i));
        }

        //简单粗暴的关闭线程池
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        businessExecutors.shutdown();
        tomcatExecutors.shutdown();
    }


    /**
     * 模拟Control任务
     */
    static class ControlThread implements Runnable {
        private int i;

        public ControlThread(int i) {
            this.i = i;
        }
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + ":" + i);
            requestIdThreadLocal.set(i);
            //使用线程池异步处理任务
            businessExecutors.submit(new BusinessTask(Thread.currentThread().getName()));
        }
    }

    /**
     * 业务任务,主要是模拟在Control控制层,提交任务到线程池执行
     */
    static class BusinessTask implements Runnable {
        private String parentThreadName;

        public BusinessTask(String parentThreadName) {
            this.parentThreadName = parentThreadName;
        }

        @Override
        public void run() {
            //如果与上面的能对应上来,则说明正确,否则失败
            System.out.println("parentThreadName:" + parentThreadName + ":" + requestIdThreadLocal.get());
        }
    }

}

结果:

pool-1-thread-2:1
pool-1-thread-4:3
pool-1-thread-1:0
pool-1-thread-3:2
pool-1-thread-5:4
pool-1-thread-6:5
pool-1-thread-7:6
pool-1-thread-8:7
pool-1-thread-9:8
pool-1-thread-10:9
parentThreadName:pool-1-thread-3:2
parentThreadName:pool-1-thread-6:5
parentThreadName:pool-1-thread-9:5
parentThreadName:pool-1-thread-10:5
parentThreadName:pool-1-thread-1:5
parentThreadName:pool-1-thread-4:5
parentThreadName:pool-1-thread-2:1
parentThreadName:pool-1-thread-5:2
parentThreadName:pool-1-thread-8:7
parentThreadName:pool-1-thread-7:6

        从这里可以出thread-7、thread-4、thread-3、thread-2、thread-1、thread-9、thread-10获取的都是6,在子线程中出现出现了线程本地变量混乱的现象,在全链路跟踪与压测出现这种情况是致命的。

怎么解决这个问题呢?TransmittableThreadLocal ”闪亮登场“。GitHub地址:GitHub - alibaba/transmittable-thread-local: 📌 TransmittableThreadLocal (TTL), the missing Java™ std lib(simple & 0-dependency) for framework/middleware, provide an enhanced InheritableThreadLocal that transmits values between threads even using thread pooling components.

TTL

        TransmittableThreadLocal何许人也,它可是阿里巴巴开源的专门解决InheritableThreadLocal的局限性,实现线程本地变量在线程池的执行过程中,能正常的访问父线程设置的线程变量。解决的是池化情况下父子线程间参数传递的问题(利用 设计模式中的 装饰者模式 对 线程中的 executor/executorService、runnable、Callable 进行封装从而达到父线程开启子线程时将父线程中的  inheritableThreadLocals 继承给 要运行的子线程(无论该线程是从线程池复用的还是新创建的))        

        TTL使用方式分为三种,修饰Runnable和Callable,修饰线程池,Java Agent来修饰JDK线程池实现类。我们主要讲解下修饰线程池的方式

示例:

首先需要在pom.xml文件中引入如下maven依赖:

 <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.12.0</version>
 </dependency>
import com.alibaba.ttl.TransmittableThreadLocal;
import com.alibaba.ttl.threadpool.TtlExecutors;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author by lianyingying
 * @Classname Service
 * @Description TODO
 * @Date 2021/10/26 10:04 上午
 * @since JDK 1.8
 * Copyright (c) 2021 imdada System Incorporated All Rights Reserved.
 */
public class Service {

    /**
     * 模拟tomcat线程池
     */
    private static ExecutorService tomcatExecutors = Executors.newFixedThreadPool(10);

    /**
     * 业务线程池,默认Control中异步任务执行线程池
     */
    private static ExecutorService businessExecutors = TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(4)); // 使用ttl线程池,该框架的使用,请查阅官方文档。

    /**
     * 线程上下文环境,模拟在Control这一层,设置环境变量,然后在这里提交一个异步任务,模拟在子线程中,是否可以访问到刚设置的环境变量值。
     */
    private static TransmittableThreadLocal<Integer> requestIdThreadLocal = new TransmittableThreadLocal<>();

//    private static InheritableThreadLocal<Integer> requestIdThreadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            tomcatExecutors.submit(new ControlThread(i));
        }

        //简单粗暴的关闭线程池
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        businessExecutors.shutdown();
        tomcatExecutors.shutdown();

    }


    /**
     * 模拟Control任务
     */
    static class ControlThread implements Runnable {
        private int i;

        public ControlThread(int i) {
            this.i = i;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + ":" + i);
            requestIdThreadLocal.set(i);

            //使用线程池异步处理任务

            businessExecutors.submit(new BusinessTask(Thread.currentThread().getName()));


        }
    }

    /**
     * 业务任务,主要是模拟在Control控制层,提交任务到线程池执行
     */
    static class BusinessTask implements Runnable {
        private String parentThreadName;

        public BusinessTask(String parentThreadName) {
            this.parentThreadName = parentThreadName;
        }

        @Override
        public void run() {
            //如果与上面的能对应上来,则说明正确,否则失败
            System.out.println("parentThreadName:" + parentThreadName + ":" + requestIdThreadLocal.get());
        }
    }
}

运行结果:

pool-1-thread-1:0
pool-1-thread-4:3
pool-1-thread-3:2
pool-1-thread-5:4
pool-1-thread-2:1
pool-1-thread-6:5
pool-1-thread-7:6
pool-1-thread-8:7
pool-1-thread-9:8
pool-1-thread-10:9
parentThreadName:pool-1-thread-10:9
parentThreadName:pool-1-thread-4:3
parentThreadName:pool-1-thread-8:7
parentThreadName:pool-1-thread-3:2
parentThreadName:pool-1-thread-5:4
parentThreadName:pool-1-thread-9:8
parentThreadName:pool-1-thread-7:6
parentThreadName:pool-1-thread-6:5
parentThreadName:pool-1-thread-1:0
parentThreadName:pool-1-thread-2:1

类图

接下来将从set方法为入口,开始探究TTL实现原理。

    public final void set(T value) {
        if (!disableIgnoreNullValueSemantics && null == value) {
            // may set null to remove value
            remove();
        } else {
            super.set(value);
            addThisToHolder();
        }
    }

代码@1:如果value为空,则调用remove()否则调用addThisToHolder()。

代码@2: 当前线程先调用父类的set方法,将value存入线程本地遍历,即Thread对象的inheritableThreadLocals中。

代码@2:调用addThisToHolder方法

那接下来重点看看这两个方法

    private void addThisToHolder() {
        if (!holder.get().containsKey(this)) {
            holder.get().put((TransmittableThreadLocal<Object>) this, null); // WeakHashMap supports null value.
        }
    }

    private void removeThisFromHolder() {
        holder.get().remove(this);
    }

代码@1:调用addThisToHolder方法,将当前ThreadLocal存储到TransmittableThreadLocal的全局静态变量holder。holder的定义如下,即该对象缓存了线程执行过程中所有的TransmittableThreadLocal对象:

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

    @Override
    protected Map<TransmittableThreadLocal<?>, ?> childValue(Map<TransmittableThreadLocal<?>, ?> parentValue) {
    return new WeakHashMap<TransmittableThreadLocal<?>, Object>(parentValue);
    }
   };

在向线程池提交任务时,会使用TtlRunnable对提交任务进行包装。接下来将重点探讨TtlRunnable。

类图

 介绍其核心属性:

  • AtomicReference capturedRef
    “捕获”的引用,根据下文的解读,该引用指向的数据结构包含了父线程在执行过程中,通过使用TransmittableThreadLocal存储的本地线程变量,但这里的触发时机是向线程池提交任务时捕获。

  • Runnable runnable
    提交到线程池中待执行的业务逻辑。

其构造方法:

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

 重点看一下子线程@1是如何“捕获”父线程中已设置的本地线程变量。

TransmittableThreadLocal$Transmitter#capture
public static Object capture() {
    Map<TransmittableThreadLocal<?>, Object> captured = new HashMap<TransmittableThreadLocal<?>, Object>();  // @1
    for (TransmittableThreadLocal<?> threadLocal : holder.get().keySet()) {     // @2
        captured.put(threadLocal, threadLocal.copyValue());                              // @3
    }
    return captured;
}

代码@1:先创建Map容器,用来存储父线程的本地线程变量,键为在父线程执行过程中使用到的TransmittableThreadLocal线程。

代码@2:holder.get(),获取父线程中使用中的ThreadLocal,因为我们从3.2.2节中发现,在当前线程在调用TransmittableThreadLocal的set方法,并且其值不为空的时候,会将TransmittableThreadLocal对象存储存储在当前线程的本地变量中。故这里使用holder.get()方法能获取父线程中已使用的ThreadLocal,并其值不为null。

代码@3:遍历父线程已使用的线程本地,将其值存入到captured中,注意默认是浅拷贝,如果需要实现深度拷贝,请重写TransmittableThreadLocal的copyValue方法。

 到这里其实已经能看到线程本地变量是如何传递的了,

接下来重点来看一下其run方法。

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);                // @1
    try {
        runnable.run();                                           // @2
    } finally {
        restore(backup);                                        // @3
    }
}

代码@1:"重放"父线程的本地环境变量,即使用从父线程中捕获过来的上下文环境,在子线程中重新执行一遍,并返回原先存在与子线程中的上下文环境变量。

代码@2:执行业务逻辑。

代码@3:恢复线程池中当前执行任务的线程的上下文环境,即代码@1,会直接继承父线程中的上下文环境,但会将原先存在该线程的线程上下文环境进行备份,在任务执行完后通过执行restore方法进行恢复。

官网地址:https://github.com/alibaba/transmittable-thread-local

  • 8
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值