InheritableThreadLocal异步传递数据实现原理

原创 吴就业 114 0 2021-02-18

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/47dca23a8b7a4b94a2f796c590eda2d0

作者:吴就业
链接:https://wujiuye.com/article/47dca23a8b7a4b94a2f796c590eda2d0
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

本篇文章写于2021年02月18日,从公众号|掘金|CSDN手工同步过来(博客搬家),本篇为原创文章。

由于上次主要分析如何解决异步获取不到Session问题,所以没有展开分析留下的那个思考题:使用InheritableThreadLocal传递Session,为什么说使用线程池不一定能获取到Session,而不是一定获取不到?《替换Shiro框架后,上线就Bug了,异步线程获取不到Session

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写入数据时,写入过程为:

因此,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只能将线程上下文传递给当前线程创建的子线程,所以只有线程池中的线程是由当前线程创建的才能够传递。

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

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

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

一、线程池是临时线程池

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

二、线程池是全局线程池

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

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

#后端

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

一种基于签名算法且简单安全的API授权机制,微信也在用

今天介绍的API授权机制或许也是使用较为广泛的一种API接口授权机制,记得笔者以前做微信支付功能的时候,微信提供的支付接口也使用这种方式:签名。

Java文件的简单读写、随机读写、NIO读写与使用MappedByteBuffer读写

面向文件编程的重要性;简单文件读写;随机访问文件读写;NIO文件读写-FileChannel;使用MappedByteBuffer读写文件。

浅谈面向文件编程(文件读写)的重要性

在我印象中,似乎很少有关于文件操作的面试题,而大多数面试题都围绕着多线程、网络编程、RPC、数据库,但其实掌握文件操作也同等重要。

替换Shiro框架后,上线就Bug了,异步线程获取不到Session

我们将原有项目的登录授权功能从Shiro切换到接入SSO单点登录服务并非一帆风顺,因为系统多了,总有一些让我们预想不到的骚操作。

如何实现SSO单点登录

随着公司业务的发展,子系统越来越多,实现SSO单点登录的需求就愈加迫切。本篇介绍笔者如何实现SSO单点登录系统。

如何并行消费Kafka拉取的数据库Binlog,提升吞吐量

本篇介绍如何并行消费Kafka拉取的数据库Binlog,以及使用Kafka订阅Binlog字段值获取防坑指南(阿里云DTS)。