从 ThreadLocal 到 TransmittableThreadLocal 的深度剖析

引言

ThreadLocal 相信大家都用过,我们在需要变量只生效于线程单位时,使用 TheadLocal 能很好的帮我们把变量在不同线程中隔离开来。本篇文章主要分析一下 ThreadLocal, InheritableThreadLocal, TransmittableThreadLocal。这三者是依次推进的关系,
在这里插入图片描述
InheritableThreadLocalThreadLocal做了拓展,TransmittableThreadLocalInheritableThreadLocal做了拓展。那接下来我们先来看下这三者中最基础的ThreadLocal

1、ThreadLocal

首先我们来验证一下 ThreadLocal 是否能够做到对线程隔离
在这里插入图片描述
结果是
在这里插入图片描述

可以看到,我们创建了一个 ThreadLocal 变量, 在主线程设置了值,然后子线程去获取值的时没有获取到对应的内容,说明主线程上设置的值在子线程看不到。然后我们再在子线程设置了其对应的值,主线程去查看时也只是看到了自己设置的值,没有看到子线程设置的值。这样我们就可以判断,对于主线程和子线程,他们各自设置的值,都不会影响到对方,也就是 ThreadLocal 做到了变量对线程隔离。

1.1、ThreadLocal 原理

在看到 ThreadLocal 的用法后我们来一起看下他的实现原理,为什么它能够做到变量对线程隔离。那么入手的起点就是ThreadLocal#set的方法了,看下他到底做了什么操作
在这里插入图片描述
哇哦,看着很简单的样子。注释的意思是:该方法给当前线程内部变量的设置对应的值,并且警示大家大部分情况下不要重载这个函数,只需要重载其中的initialValue函数就可以给这个变量设置值。

看注释的意思非常明显了,原来 ThreadLocal 的变量值存储在 Thread 变量上,那当然对于线程是隔离的咯。那么我们来看下这个 getMap(t)干了啥。
在这里插入图片描述
哇哦哦,就是取了当前线程上的变量而已,那么当这个变量一开始不存在时,ThreadLocal 又做了哪些工作呢?我们来一起看下 CreateMap(t, value)函数
在这里插入图片描述
额,也是非常的简单,就是给线程变量附了初值,但是这里我们要注意一点,就是这个Map的key就是我们的 ThreadLocal 本身。

所以我们可以总结一下,因为不同的线程上拥有自己的变量来保存这个 ThreadLocal 值,所以他能够做到对于线程隔离的。

1.2、ThreadLocal 的内存泄漏问题

这个问题也是老生常谈了,这里笔者也说下自己的理解,为啥会发生内存泄漏呢?内存泄漏指的是本应该被回收的值,因为其引用一直存在,使得垃圾回收器不能够将其进行回收,随着程序的运行,这样的值越来越多,最终导致系统OOM。那么 ThreadLocal中为什么会出现内存泄漏,这个归根到底还是由于 ThreadLocalMap 的引用类型。

在这里插入图片描述
我们知道ThreadLocal 变量是TheadLocalMap结构的。上面是ThreadLocalMap的构造函数,其内部是由Entry列表来保存数据的,那我们来看下Entry的构造
在这里插入图片描述
注意下面的表述有点拗口,但是希望大家可以仔细思考一下:
通过上面的截图可以知道这个 Entry的key给予了ThreadLocal弱引用,如果这个ThreadLocal对象没有其他强引用或者软引用,那么在下一次GC时,就会回收这个ThreadLocal对象(其实这样表述也不是很准确,如果这个对象太大,那么会直接放入到老年代,那只能到下一次的Full GC时才会被回收,在Minor GC时并不会被回收)。那么这个时候这个Entry上对应的key就为null,而value因为没有设置为弱引用,那么一直会有ThreadLocalMap这个强引用他,那么这个value的生命周期就会和ThreadLocalMap一样,应为ThreadLocalMap是线程上的变量,所以只要这个线程还存活,那么这个value就一直会在。

在这里插入图片描述
什么场景下会内存泄漏
在线程一直存在的情况下,并且每一次这个线程过来,都会重新创建一个TheadLocal对象作为key,并且在线程执行结束后,这个ThreadLocal对象原本函数中创建时存放在栈中的强引用就不再会有了,从而这个ThreadLocal就只存在一个弱引用,从而每一次这个线程操作依次,就会往Thead对象中的theadLocals(这个就是上面说的ThreadLocalMap类型的对象)添加一个数据,并且这个数据最后的key为null,value会一直随着这个线程存在。这样就导致了这一块数据一直得不到回收,最终引起了程序的OOM。

对于上面可能会有小伙伴会问,为什么会存在线程一直存在呢?原因就是现在我们都是使用了池化技术来减少计算机性能的消耗,在线程方面,一般都不会直接创建线程,而是直接使用线程池中的线程来处理,而线程池中的线程是不会被回收的,在一次线程执行完后,会直接放入到线程池中,等待下一次线程的调用。

如果避免内存泄漏
其实也非常简单,我们在线程调用时,不在重新创建ThreadLocal对象,使得全局创建一个这样的对象,就可以避免这个问题。
在这里插入图片描述
就比如这样,这个对象在程序存续间一直存在强引用,也就不会被回收掉,并且即使在线程用完之后没有对这个ThreadLocal对象进行remove操作,下一次set的时候因为threadLocal对象相同,那么往threadLocals中设置值时因为key相同也不会新增数据,而是直接覆盖数据。

但是奥,要注意,我们还是要在一次任务操作完后调用ThreadLocalremove操作。为什么呢?因为我们如果这次没有删除,因为线程池的原因,在这个线程下一次调用时,就会存在脏数据问题。所以我们要切记在一次调用结束后都要对ThreadLocal对象进行remove操作。

2、InheritableThreadLocal

我们看到ThreadLocal就已经非常厉害了,那为啥还需要InheritableThreadLocal这个类呢?原因就是ThreadLocal在父子线程情况下就无能为力了。我们看一个例子
在这里插入图片描述
运行结果是在这里插入图片描述
这说明了啥,就是主线程设置的值,在子线程就找不到了,这可咋整,所以InheritableThreadLocal就这样出生了。让我们一起看下这个类
在这里插入图片描述
哇哦,非常的简单,我们可以注意到,就是存储ThreadLocal的对象由原来的threadLocals变成了现在的inheritableThreadLocals,我们在Thread对象中都可以直接看到这两个值
在这里插入图片描述
为啥从inheritableThreadLocals上取值时子线程就能取得到值呢?其实秘密就在创建线程的时候

java.lang.Thread#init

在这里插入图片描述
好家伙了吧,直接在创建线程的时候把值从父线程中复制到子线程上了。这样在子线程处也可以看得到对应的内容。

3、TransmittableThreadLocal

有了上面这两种ThreadLocal,居然还有TransmittableThreadLocal,原因就是我们现在一般都不是直接创建线程了呀,而是直接通过线程池,那么我们在调用线程池中的线程时,是不是也需要将主线程中的数据传递到这个子线程中,处理方式就是在TransmittableThreadLocal中。我们先来看一个例子
在这里插入图片描述
这个代码运行的结果是
在这里插入图片描述
可以发现,我们确实在线程池运行程序时获取到了主线程设置的值。并且查看上面的代码,其他地方都差不多,就是有一处代码和平时不太一样。
在这里插入图片描述
就是这一行代码了,他把Runnable的对象封装了一下,然后再让线程执行器去执行这个任务。看样子是这个封装后的任务对象做了某种操作使得需要运行的线程可以得到主线程中设置的值。其实我们也可以分析一下,如果不是在传入的任务那里做了某种操作,其他地方也没法从线程池中取出任意一个线程时将主线程中的值设置到这个线程中,因为在线程执行器准备执行任务时,只有传入的参数能够知道当前执行的是哪一个线程。 所以我们一起来看下这个TtlRunnable具体做了哪些操作。
在这里插入图片描述
通过代码可以知道,调用的get函数返回了TtlRunnable这个实现了Runnable的类。来看下他的构造函数
在这里插入图片描述
多了两个参数,capturedRefreleaseTtlValueReferenceAfterRun,在看一下这个TtlRunnable运行起来后做了什么操作。
在这里插入图片描述
这个releaseTtlValueReferenceAfterRun看着主要是用来表示状态作用的,我们需要把主要目标放在capturedRef上,这个capturedRefTtlRunnable构造时通过capture()函数来获取值,然后再运行时通过replay(object)来获取值。说明了数据主要是通过这两个函数来存储和获取的。

com.alibaba.ttl.TransmittableThreadLocal.Transmitter#capture

在这里插入图片描述

com.alibaba.ttl.TransmittableThreadLocal.Transmitter#replay

在这里插入图片描述
capture上保存了threadLocal值的快照,然后在replay中会将其重放,这个细讲也不展开了,因为终其原理就是set和get的操作,这里只分享一个点哦

com.alibaba.ttl.TransmittableThreadLocal.Transmitter#replayTtlValues

在这里插入图片描述
就是重放的过程中,会对数据进行筛查,只返回capture中捕获到数据,对于其他原来已经存放着的数据,会进行删除,也就是说,理论上分析我们在使用TransmittableThreadLocal设置完值后不需要在手动调用,但是啊,建议大家还是在程序运行完之后手动调用remove方法来清除数据,咱们要把问题解决控制在自己手里才是最安心的。

以上就是笔者对于ThreadLocal的一些个人理解,如果有问题的地方,欢迎大家批评指正。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值