java父子线程,变量传递问题解析

createh52周前 (05-12)技术教程5

一,ThreadLocal 存储的线程变量不能在父子线程中传递

项目中,我们经常会用ThreadLocal来存储线程变量,方便后续业务操作的获取。但是如果后续方法中又重新开线程去处理业务的时候,ThreadLocal是不能正常获取到存储的线程变量的。也就是线程变量不能在父子线程中传递

JDK提供的解决方案InheritableThreadLocal

InheritableThreadLocal是jdk自带地提供父子线程传递的实现类。

InheritableThreadLocal实现的原理是:在创建线程时会去赋值父线程的InheritableThreadLocal中的值,来达到父子线程变量传递的目的。

Thread类源码里面的两个变量传递桥梁

/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;


    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;


JDK线程池好处和直接new Thread危害解释

说明:使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。


所以业务中我们都是用线程池去处理异步业务。但是由于线程池的线程是池化复用,InheritableThreadLocal只有在创建线程Thread时才会去赋值父线程的InheritableThreadLocal中的值,所以当我们用线程池执行异步业务任务的时候,父子线程变量的传递就会失效。因此InheritableThreadLocal并不能满足我们的业务使用。

alibaba提供的解决方案:

笔者看了下InheritableThreadLocal和ThreadLocal的源码,并收集了一些资料,发现这个问题在alibaba开源项目已经提供了解决方案。脑子里浮现了一句:“想做第一个吃螃蟹的人好难”。前人已经把坑填好了。

这里我们介绍alibaba开源的解决方案:

Github地址:

https://github.com/alibaba/transmittable-thread-local

感兴趣的小伙伴可以下载看下组件的实现原理。


二,开源组件简单介绍

JDK的InheritableThreadLocal类可以完成父线程到子线程的值传递。但对于使用线程池等会池化复用线程的执行组件的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把 任务提交给线程池时的ThreadLocal值传递到任务执行时。

alibaba提供的TransmittableThreadLocal类继承并加强InheritableThreadLocal类,解决上述的问题。

下面是组件几个典型场景例子

  1. 分布式跟踪系统 或 全链路压测(即链路打标)
  2. 日志收集记录系统上下文
  3. Session级Cache
  4. 应用容器或上层框架跨应用代码给下层SDK传递信息

使用TransmittableThreadLocal的三种方式

  1. 使用TtlRunnable和TtlCallable来修饰传入线程池的Runnable和Callable。
  2. 修饰线程池,通过工具类TtlExecutors完成。
  3. 使用Java Agent来修饰JDK线程池实现类。这种方式,实现线程池的传递是透明的,业务代码中没有修饰Runnable或是线程池的代码。即可以做到应用代码无侵入。

三,使用演示

这里我们采用TtlRunnable和TtlCallable来修饰传入线程池的Runnable和Callable的方式来解决我们的业务问题。

第一步:编写TransmittableThreadLocal上下文

public class AppIdTtlContext {
  private static TransmittableThreadLocal<String> appIdLocal = new TransmittableThreadLocal<>();
  public static void addAppId(String appId) {
      appIdLocal.set(appId);
  }
  public static String getAppId() {
      return appIdLocal.get();
  }
  public static void removeAppId() {
      appIdLocal.remove();
  }
}

第二步:编写线程池

线程池的核心线程数和最大线程数我们设置为1,方便看实验结果,并使用TtlRunnable和TtlCallable来修饰传入线程池的Runnable和Callable

public class TtLThreadPoolUtil {
    private static ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
    static{
        taskExecutor.setCorePoolSize(1);
        taskExecutor.setMaxPoolSize(1);
        //任务队列最大长度
        taskExecutor.setQueueCapacity(200);
        taskExecutor.setAllowCoreThreadTimeOut(true);
        taskExecutor.setKeepAliveSeconds(2000);
        taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        taskExecutor.initialize();
    }


    private TtLThreadPoolUtil(){
    }


    public static void addTask(Runnable task) {
        //利用淘宝的TtlRunnable包装一下
        TtlRunnable ttlRunnable = TtlRunnable.get(task);
        taskExecutor.execute(ttlRunnable);
    }


    //Callable任务
    public static Future addReturnedTask(Callable task){
        //利用淘宝的TtlCallable包装一下
        Callable ttlCallable = TtlCallable.get(task);
        return taskExecutor.submit(ttlCallable);
    }


    public static void shutdown() {
        try{
            taskExecutor.shutdown();
        }catch(Exception ex) {
        }
    }
}

第三步:编写拦截器

编写拦截器实现线程变量的赋值,这里我们赋值的是header头的一个appid

public class OrderInterceptor  implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String appid = request.getHeader("appid");
        AppIdTtlContext.addAppId(appid);
        return true;
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
        AppIdTtlContext.removeAppId();
    }
}

第四步:编写controller的方法,来验证效果

@RequestMapping("/testTtlThread")
@ResponseBody
public void testTtlThread() {
    Long idParent = Thread.currentThread().getId();
    String nameParent = Thread.currentThread().getName();
    System.out.println( idParent+nameParent+"controller线程获取到的appid为="+ AppIdTtlContext.getAppId());
    TtLThreadPoolUtil.addTask(()->{
        Long id = Thread.currentThread().getId();
        String name = Thread.currentThread().getName();
        System.out.println( id+name+"异步线程获取到的appid为="+AppIdTtlContext.getAppId());
    });
}

第五步:实验结果

  1. 请求接口http://127.0.0.1:8081/testTtlThread 并且header头的appid值为1002,可以看到appid能被子线程正常获取。
  2. 请求接口http://127.0.0.1:8081/testTtlThread 并且header头的appid值为1003,可以看到appid能被子线程正常获取,并且线程池用的是同一个线程,且不会受到第一次请求的干扰。



四,使用TtlRunnable修饰传入线程池的Runnable原理解析

TtlRunnable传递原理

每次执行runnable之前,都必须先获取一个包装过的TtlRunnable,此过程会给TransmittableThreadLocal赋值。并通过Snapshot类的两个字段属性来实现父子变量的值复制和传递。

源码剖析

获取TtlRunnable对象

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

封装capturedRef对象

@NonNull
public static Object capture() {
    return new Snapshot(captureTtlValues(), captureThreadLocalValues());
}

captureTtlValues

方法里面对TransmittableThreadLocal复制赋值

private static HashMap<TransmittableThreadLocal<Object>, Object> captureTtlValues() {
    HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value = new HashMap<TransmittableThreadLocal<Object>, Object>();
    for (TransmittableThreadLocal<Object> threadLocal : holder.get().keySet()) {
        ttl2Value.put(threadLocal, threadLocal.copyValue());
    }
    return ttl2Value;
}

最后返回组装好的Snapshot对象,达到变量传递的目的

privatestatic class Snapshot {
  final HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value;
  final HashMap<ThreadLocal<Object>, Object> threadLocal2Value;
  private Snapshot(HashMap<TransmittableThreadLocal<Object>, Object> ttl2Value, HashMap<ThreadLocal<Object>, Object> threadLocal2Value) {
      this.ttl2Value = ttl2Value;
      this.threadLocal2Value = threadLocal2Value;
  }
}

五,小结

  1. ThreadLocal 实现线程内部变量共享。
  2. InheritableThreadLocal 实现了父线程与子线程的变量传递。InheritableThreadLocal 无法解决在使用线程池等池化复用线程的执行组件,异步执行执行任务,需要传递上下文的情况。
  3. 淘宝开源的TransmittableThreadLocal解决了使用线程池等池化复用线程的执行组件,异步执行执行任务,需要传递上下文的情况。

最后说一句

感谢您的阅读,您的正反馈是我持续创作的动力,十分期待欢迎您的关注!

相关文章

Java 的变量类型

Java 中的变量分为两种,一种是基本类型,一种是引用类型。Java 的变量定义方式和 C 语言相似,类型在前,变量名在后。比如,定义一个整型变量:int answer = 42;变量的意思是,它的值...

java.io.File中的四个静态分隔符变量

java.io.File类包含四个静态分隔符变量。在这里,我们将了解它们以及何时使用它。分别是separator、separatorChar、pathSeparator 、pathSeparatorC...

【性能篇】关于Java性能调优你了解吗

关于Java性能调优分为两方面的优化,一方面是针对Java虚拟机内存的调优,一方面是数据库DB的调优。今天我们主要讲解Java虚拟机内存的调优,在实际开发中,几乎不可能通过单纯的调优来达到消除GC的目...

配置Java环境变量:(WIN7为例)

1.JAVA_HOME变量的设置 2.Path变量的设置 3.ClassPath变量的设置二、JDK安装群文件下载好之后,进入文件夹,双击根据提示进行安装,直至安装完成。(建议默认地址,一下以默认...

jdk环境变量的配置

1.右击打次电脑属性,进入高级系统设置.选择高级 点击环境变量2.系统变量 新建 变量名上面输入JAVA_HOME 对应的变量值则找到jdk的安装目录3.找到系统变量中的path,点击编辑,建议在最前...