引言
ThreadLocal
相信大家都用过,我们在需要变量只生效于线程单位时,使用 TheadLocal 能很好的帮我们把变量在不同线程中隔离开来。本篇文章主要分析一下 ThreadLocal
, InheritableThreadLocal
, TransmittableThreadLocal
。这三者是依次推进的关系,
InheritableThreadLocal
对ThreadLocal
做了拓展,TransmittableThreadLocal
对InheritableThreadLocal
做了拓展。那接下来我们先来看下这三者中最基础的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相同也不会新增数据,而是直接覆盖数据。
但是奥,要注意,我们还是要在一次任务操作完后调用ThreadLocal
的remove
操作。为什么呢?因为我们如果这次没有删除,因为线程池的原因,在这个线程下一次调用时,就会存在脏数据问题。所以我们要切记在一次调用结束后都要对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
的类。来看下他的构造函数
多了两个参数,capturedRef
和releaseTtlValueReferenceAfterRun
,在看一下这个TtlRunnable
运行起来后做了什么操作。
这个releaseTtlValueReferenceAfterRun
看着主要是用来表示状态作用的,我们需要把主要目标放在capturedRef
上,这个capturedRef
在TtlRunnable
构造时通过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
的一些个人理解,如果有问题的地方,欢迎大家批评指正。