实战:一文搞懂InheritableThreadLocal

概叙

科普:一文搞懂ThreadLocal-CSDN博客

前面我们整理了ThreadLocal,为了齐全完美,再把InheritableThreadLocal和FastThreadLocal也都整理一下。

吐槽一下csdn,“一文搞懂ThreadLocal”中刚发现有缺漏的地方,就修改了一下,结果就是一直待审核。再回头继续说InheritableThreadLocal。

前文我们详细介绍了ThreadLocal原理及设计,从源码层面上分析了ThreadLocal。但由于ThreadLocal设计之初就是为了绑定当前线程,如果希望当前线程的ThreadLocal能够被子线程使用,实现方式就会相当困难(需要用户自己在代码中传递)。在此背景下,nheritableThreadLocal应运而生。(ThreadLocal 无法被继承,所以就有了InheritableThreadLocal完成在子线程中继承父线程的值,这个值源自父线程,是子线程中父线程的副本,子线程对其的操作,不会影响父线程中的值

Inheritable thread-local variables are used in preference to ordinary thread-local variables when the per-thread-attribute being maintained in the variable (e.g., User ID, Transaction ID) must be automatically transmitted to any child threads that are created.

一般应用:

调用链追踪:在调用链系统设计中,为了优化系统运行速度,会使用多线程编程,为了保证调用链ID能够自然的在多线程间传递,需要考虑ThreadLocal传递问题(大多数系统会使用线程池技术,这已经不仅仅是InheritableThreadLocal能够解决的了,我会在另外一篇文章中介绍相关技术实现)。

看下面的示例

从上面示例中可以看出,子线程中获取到了主线程中的变量值,虽然子线程修改了,但是主线程中的值不受影响,改的只是子线程中的副本。同一个全局静态变量,为啥呢?

接下来我们就去看一下InheritableThreadLocal的原理。

InheritableThreadLocal原理(父子线程间传值)

0.getMap 与 createMap

看 InheritableThreadLocal 的源码就几句话往事,很简单的重载了 getMap 与 createMap。无论是getMap读取,还是createMap创建,源头都是t.inheritableThreadLocals,也就是Thread对象。

InheritableThreadLocal类重写了ThreadLocal的3个函数:

    /**
     * 该函数在父线程创建子线程,向子线程复制InheritableThreadLocal变量时使用
     */
    protected T childValue(T parentValue) {
        return parentValue;
    }

    /**
     * 由于重写了getMap,操作InheritableThreadLocal时,
     * 将只影响Thread类中的inheritableThreadLocals变量,
     * 与threadLocals变量不再有关系
     */
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    /**
     * 类似于getMap,操作InheritableThreadLocal时,
     * 将只影响Thread类中的inheritableThreadLocals变量,
     * 与threadLocals变量不再有关系
     */
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }

1.Thread初始化int()方法

我们每new Thread,都会去执行init()初始化一个线程。

可以看到源码中,初始化一个线程. ,有两处调用最终的init方法,

1、上面的 init(),不传AccessControlContext,inheritThreadLocals=true

2、传递AccessControlContext,inheritThreadLocals=false。

我们默认 new Thread都是inheritThreadLocals=true,采用默认方式产生子线程时,inheritThreadLocals=true;若此时父线程inheritableThreadLocals不为空,则将父线程inheritableThreadLocals传递至子线程。

2.Thread初始化int()方法ThreadLocal.createInheritedMap()

this.inheritableThreadLocals =
    ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

让我们继续追踪createInheritedMap:

3.ThreadLocal.createInheritedMap()

(当前线程就是父线程??? 其实是谁调用这个函数赋值inheritableThreadLocals,谁就是父线程)

this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

其实到这里inheritableThreadLocals的原理就结束了,这个地方就是把父线程的inheritableThreadLocals拷贝一份给子线程的inheritableThreadLocals。

   // createInheritedMap 其实就是new ThreadLocalMap(parentMap)
   static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }

        /**
         * 构建一个包含所有parentMap中Inheritable ThreadLocals的ThreadLocalMap
         * 该函数只被 createInheritedMap() 调用.
         * new ThreadLocalMap(parentMap) 其实就是逐一复制,不是浅拷贝,深度拷贝。
         */
        private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            // ThreadLocalMap 使用 Entry[] table 存储ThreadLocal
            table = new Entry[len];

            // 逐一复制 parentMap 的记录
            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        // 可能会有同学好奇此处为何使用childValue,而不是直接赋值,
                        // 毕竟childValue内部也是直接将e.value返回;
                        // 个人理解,主要为了减轻阅读代码的难度
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

从ThreadLocalMap可知,子线程将parentMap中的所有记录逐一复制至自身线程。

4.小结

重点就是if里面的逻辑。

if (inheritThreadLocals && parent.inheritableThreadLocals != null)

第一项inheritThreadLocals 是传进来的boolean值,重载时传的是true,所以满足条件。

第二项就是判断父线程中的inheritableThreadLocals 是不是空,如果不是空就满足条件。

当同时满足if的两个条件后,就执行

this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

新创建出来的子线程的inheritableThreadLocals 变量就和父线程的inheritableThreadLocals 的内容一样了。

以上就是从源码的角度分析InheritableThreadLocal的原理。

InheritableThreadLocal中的坑和填坑

InheritableThreadLocal是ThreadLocal的一个子类,它的设计目的是为了让线程池中的线程能够从创建线程的线程中继承ThreadLocal变量。然而,在使用线程池时,错误的使用InheritableThreadLocal可能会导致意外的行为。

以下是一些使用InheritableThreadLocal时可能会遇到的问题和解决方法:

1.内存泄漏

ThreadLocal一样,存在内存泄漏的风险,子线程记得及时remove。

内存泄漏:如果在使用完InheritableThreadLocal设置的变量后没有显式地移除(remove()),可能会导致内存泄漏。

解决方法:确保在finally块中移除InheritableThreadLocal变量。

2.不正确的继承:

不正确的继承:如果在线程池中的某个线程在执行完任务之后,并没有立即退出,而是被线程池再次利用来执行其他任务,那么新任务有可能会错误地继承之前任务设置的InheritableThreadLocal变量。

解决方法:确保每个任务执行完毕后,都重置InheritableThreadLocal变量,或者在任务执行前后进行适当的清理和设置。

3.不正确的构造函数:

不正确的构造函数:如果在创建InheritableThreadLocal实例时不正确地覆写childValue方法,可能会导致意外的继承行为。

解决方法:确保InheritableThreadLocal的子类化是正确的,并且覆写childValue方法时遵循了父类的合同。

总结,在使用InheritableThreadLocal时,需要特别注意内存泄漏和错误的变量继承,并确保每个任务执行完毕后,InheritableThreadLocal的设置和清理都是正确的。

4.SpringMVC中的request为空(线程池与InheritableThreadLocal

某天测试环境更新后,有小伙伴反应页面会随机性的发生请求参数为空的情况(request.getParamter为空),但是前端的参数是传了的,而且不能稳定重现,需要在页面上经过一番操作之后才会发生,而当问题重现之后,之前那些可用的页面就变得不可用了,然后就会在可用和不可用之间交替......

4.1寻找罪魁祸首

代码中request为空,但是前端有传递,第一时间想到的就是线程切换导致ThreadLocal传递出现问题。

然而这个坑我们之前是踩过的,并且已经在切面中手动改成了可继承的线程变量

HttpServletRequest servletRequest = WebUtil.getRequest();
HttpServletResponse servletResponse = WebUtil.getResponse();
//声明子线程的时候,这些属性不会继承,手动赋值成可继承的属性
ServletRequestAttributes attributes = new ServletRequestAttributes(servletRequest, servletResponse);
RequestContextHolder.setRequestAttributes(attributes, true);
LocaleContextHolder.setLocaleContext(LocaleContextHolder.getLocaleContext(), true);

难道切面没生效?

可是经过调试发现,这段代码是进入并执行了的。

通过查看提交记录发现,切面中有人加了这么一段代码(没错就是我)

ExecutorService TIMEOUT_EXECUTOR_POOL = new ThreadPoolExecutor(
    Runtime.getRuntime().availableProcessors() + 1, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
    new SynchronousQueue<>(), ThreadUtil.newNamedThreadFactory("TIMEOUT_EXECUTOR_POOL", false)
);
FutureTask<Object> futureTask = new FutureTask<>(() -> {
    try {
        return joinPoint.proceed();
    } catch (Exception ex) {
        throw ex;
    } catch (Throwable throwable) {
        throw new Exception(throwable);
    }
});
TIMEOUT_EXECUTOR_POOL.submit(futureTask);

为了增加超时时间的控制,我用FutureTask把执行的代码包装了一层

在这里打断点调试,发现在报错的时候,futureTask外部request参数有值,进入后参数为空。

但是,偶尔也是会有值的!有值的时候就是页面正常的时候。

4.2 找出作案动机(原因)

我们先看下InheritableThreadLocal是怎么实现线程变量可继承的

在Thread的init()方法中有一段代码

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    //省略部分代码
   
    //如果父线程inheritableThreadLocals不为空,则保存下来
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    
    //省略部分代码
}

可以看到InheritableThreadLocal是在Thread创建的时候继承的。

而我们知道线程池的作用就是“缓存”线程来避免线程频繁的创建和销毁,所以如果在线程池中使用InheritableThreadLocal,只有第一个创建线程时的请求是可以用的,后续请求的InheritableThreadLocal都跟第一个请求一样,不会再改变。

至此,问题原因找到了,因为我创建线程池的时候初始化了CPU核数+1个线程,所以开始一些请求是正常的,后续当这些线程都使用了之后,就会因为InheritableThreadLocal不同导致错误。而且我们自己测试的时候是在几个按钮中重复点击,如果线程的第一个请求是/user/query,当你再次发起这个请求的时候如果刚好分配的是这个线程,页面就是正常的,于是就出现页面时好时坏的情况.

4.3 填坑(解决方案:要么不用,要么用新人TransmittableThreadLocal)

OK,出现问题的地方找到了,下面来解决

1、直接注释掉这段超时控制的代码

这个实在是太粗暴了,只适合紧急情况下使用,作为一个有追求的程序猿,我是不可能这么做的

2、不用线程池,直接new Thread

既然是线程池复用导致的问题,不用线程池就可以解决

3、使用阿里的TransmittableThreadLocal

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

阿里巴巴开源了一个类似于InheritableThreadLocal的库,就是用来在线程池中使用,有兴趣的可以瞅一眼

3.InheritableThreadLocal和线程池的恩怨

上面springMVC中的问题,其实就是线程池导致的。

子线程初始化的时候,将父线程的InheritableThreadLocal给继承过来。这种场景,在不使用线程池的情况是没有问题的。但是如果搭配上了线程池,就会存在问题。这里我们先简单介绍一下线程池的作用机理。

其中最关键的点在于,线程池会复用原有线程,致使部分线程不会经过Init初始化的过程,InheritableThreadLocal的值也就没有办法得到更新。最终造成了错误的数据传递。

再看看这个例子:

InheritableThreadLocal和线程池搭配使用时,可能得不到想要的结果,因为线程池中的线程是复用的,并没有重新初始化线程,InheritableThreadLocal之所以起作用是因为在Thread类中最终会调用init()方法去把InheritableThreadLocal的map复制到子线程中。由于线程池复用了已有线程,所以没有调用init()方法这个过程,也就不能将父线程中的InheritableThreadLocal值传给子线程。

从上图可以看出,我们在main线程中第二次set并没有被第二次submit的线程get到。也印证了我们的结论。

优势:

1、通过在线程初始化的时候传递相应的ThreadLocal变量,解决了非线程池下的异步线程的变量传递问题。

劣势:

1、线程池复用线程和ITL底层机制无法兼容,导致了ITL无法结合线程池发挥作用。

解决方案:

InheritableThreadLocal和线程池搭配使用的问题,解决办法就是自己动手,丰衣足食,自己写一个InheritableThreadLocal,解决不了问题,就直接干掉有问题的人,搞个新人。(社会学上,这是一个非常恐怖的事情。

1、InheritableThreadLocal在线程池下是无效的,原因是只有在创建Thread时才会去复制父线程存放在InheritableThreadLocal中的值,而线程池场景下,主业务线程仅仅是将提交任务到任务队列中。
2、如果需要解决这个问题,可以自定义一个RunTask类,使用反射加代理的方式来实现业务主线程存放在InheritableThreadLocal中值的间接复制。( 王二北 大佬的思路确实高明)

确实线程池中每次每个线程都跟着主线的修改而修改了,

参考解决案例:

InheritableThreadLocal遇到线程池出现的问题分析及解决方案_inheritablethreadlocal使用线程池的解决方式-CSDN博客

https://zhuanlan.zhihu.com/p/473659523

遇到线程池InheritableThreadLocal就废了,该怎么办? - 简书

  • 33
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

-无-为-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值