首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >InheritableThreadLocal异步传递数据实现原理

InheritableThreadLocal异步传递数据实现原理

作者头像
吴就业
发布2021-03-01 13:02:57
5650
发布2021-03-01 13:02:57
举报
文章被收录于专栏:Java艺术Java艺术

Java中,一个Java线程就是一个操作系统线程,创建一个线程需要通过new Thread创建,由JVMThread绑定操作系统线程,即便是使用线程池,也需要通过new Thread创建线程。

Thread类有两个ThreadLocal字段:

public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

InheritableThreadLocalThreadLocal的子类,本质上就是一个ThreadLocal

Thread类中,threadLocalsinheritableThreadLocals都是线程对象私有的,只能通过当前线程对象写入和获取数据,只是Thread会将写入inheritableThreadLocals的数据传递给子线程的inheritableThreadLocals

当我们往ThreadLocal或者InheritableThreadLocal写入数据时,写入过程为:

  • 1、ThreadLocal或者InheritableThreadLocal先调用Thread#currentThread静态方法获取当前线程的Thread对象;
  • 2、获取Thread对象的threadLocals或者inheritableThreadLocals
  • 3、将ThreadLocal或者InheritableThreadLocal对象作为key,将数据写入到当前Thread对象的threadLocals或者inheritableThreadLocals字段中。

因此,ThreadthreadLocalsinheritableThreadLocalskeyThreadLocal或者InheritableThreadLocal实例,value是写入的数据。

关于threadLocals我在前面一篇《反向理解ThreadLocal,或许这样更容易理解》已经详细介绍过了,本篇重点分析inheritableThreadLocals是如何传递给子线程的。

默认情况下,当我们使用new Thread()创建一个线程时,在Thread的构造方法中会通过Thread#currentThread获取当前线程,将当前线程作为新创建线程的父线程,所以就有了父子线程关系。

无论使用哪个重载的构造方法创建Thread,都会在构造方法中调用init方法完成初始化为Thread字段赋值,而init方法中有这样一段代码:

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
        ......
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
       ......
}

init方法中,由于inheritThreadLocals参数默认为true,所以只要父线程的inheritableThreadLocals字段不为空,就copy一份父线程的inheritableThreadLocals给当前创建的线程对象,这就实现了将父线程的inheritableThreadLocals存储的数据传递给子线程。

使用InheritableThreadLocal我们不得不考虑的问题:内存泄漏。

ThreadLocal.ThreadLocalMap使用数组存储元素,与HashMap不同,它通过开放定址法解决hash冲突,不存在链表,通过动态扩容数组可无限存储元素,数组元素的类型为Entry

当我们往ThreadLocal.ThreadLocalMap写入一个key-value时,ThreadLocalMapkeyvalue包装成一个Entry,并通过keyhashcode值计算索引值,将Entry放到数组中。

ThreadLocal.ThreadLocalMap.Entry类的源码如下:

static class Entry extends WeakReference<ThreadLocal<?>> {
   Object value;
   Entry(ThreadLocal<?> k, Object v) {
       super(k);
       value = v;
   }
}

虽然key为弱引用的ThreadLocal,当ThreadLocal释放时,Entrykey变为null,但由于value还在,如果Thread不释放,那么Entry也就不会被垃圾收集器回收。

但如果线程是临时创建的,在方法中创建且没有被其它地方引用,当线程执行完成时就会被JVM销毁,在线程实际退出之前由JVM调用线程的exit方法给线程对象完成清理。exit方法部分源码如下。

private void exit() {
    ......
    threadLocals = null;
    inheritableThreadLocals = null;
    ......
}

因此,只要Thread对象的exit方法被调用,就不会存在内存泄漏问题。只要线程用完就销毁,那么使用InheritableThreadLocal,在子线程中不需要调用InheritableThreadLocalremove方法也不会存在内存泄漏的可能。

比如我们在项目中使用InheritableThreadLocal实现将Session传递给子线程:

@GetMapping("/test")
public SsoUser test() {
    // 获取登录用户
    SsoUser ssoUser = SsoUserManager.curLoggedUser();
    System.out.println(ssoUser.getUserCode());
    // 支持子线程传递
    new Thread(() -> {
        try {
            Thread.sleep(100);
            SsoUser ssoUser2 = SsoUserManager.curLoggedUser();
            System.out.println(ssoUser2.getUserCode());
        } catch (InterruptedException e) {
        }
    }).start();
    return ssoUser;
}

在此案例中,由于子线程只是临时创建的,所以我们不需要在子线程中调用InheritableThreadLocalremove方法,只需要在父线程调用一次remove方法,因为tomcatwork线程是不会在一次请求结束后就销毁的。

现在我们已经知道了InheritableThreadLocal是如何实现将数据传递给子线程的,思考题的答案也就有了一半:由于InheritableThreadLocal只能将线程上下文传递给当前线程创建的子线程,所以只有线程池中的线程是由当前线程创建的才能够传递。

但要知道另一半答案我们还需要从线程池中寻找。

使用不同参数构建的线程池不同,常见的有单线程的线程池、只有固定数量核心线程的线程池、有固定数量核心线程和非核心线程的线程池、只有非核心线程的线程池。

线程池的几个构造参数说明如下:

  • corePoolSize:核心线程数,不会被释放的线程数量(设置allowCoreThreadTimeOutture时例外);
  • maximumPoolSize:线程池的最大线程数,等于核心线程与非核心线程的数量总和;
  • keepAliveTime:非核心线程最大空闲等待时间,在指定空闲时间后如果还没有任务则释放该线程;
  • workQueue:任务队列,当核心线程数用完时,任务被放入队列。

一、线程池是临时线程池

如果线程池是在当前线程创建的,且任务都是由当前线程提交的,线程池用完就消毁了,那么不管是哪种线程池,池中的线程都是由当前线程所创建,在这种场景下,InheritableThreadLocal能够将Context传给给线程池中的任一线程。

二、线程池是全局线程池

如果线程池是全局线程池:

  • 没有核心线程且非核心线程的keepAliveTime等于0:线程都是用到才创建,且由于keepAliveTime等于0,线程用完可能就释放了,在这种场景下,相当于是由当前线程创建子线程执行任务,因此能够实现透传;
  • 没有非核心线程:前(核心线程数)个任务的提交都会创建线程,也都是由当前线程创建,所以只有这几个任务的执行是能够正常获取父线程写入InheritableThreadLocal的数据的,后面提交的任务就不知道会被哪个核心线程拉取执行了;
  • 其它:....

因此,如果线程池是全局线程池,那么无论是哪个情况,都不建议使用InheritableThreadLocal

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-02-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java艺术 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档