一、概括说一句
ThreadLocal提供了线程本地变量,也就是当创建了一个ThreadLocal变量,那么访问这个变量的每个线程都拥有了这个变量的本地拷贝。多个变量同时操作这个变量时,实际都是在操作自己的本地内存里面的变量,从而避免现成的安全问题。
二、走进看一看
概括的这句话从字面上看起来很容易理解,但是真正理解似乎并不是那么容易。
先来一个小栗子。
一个简单的springBoot工程。
过滤器:获取当前线程id,并添加绑定。
拦截器:前处理和后处理分别打印绑定的数据。
主角登场,绑定,获取和移除都有它来处理。
终点站:控制器也输出一下 绑定的 线程id。
接下来执行一下吧!
过滤器,前处理,控制器,后处理及最后移除绑定!例子很简单,相信大家都熟悉,在权限校验,用户信息获取等session场合,似乎同学们都没有被并发下的安全问题发过愁,毕竟ThreadLocal实现了线程封闭嘛!
现实案例很多,举一个SpringWeb中的案例:直接获取HttpServletRequest对象,让我的小栗子能站在巨人的肩膀上。
在Service方法中使用HttpServletRequest的API,但是又不想把HttpServletRequest对象当作这个Service方法的参数传过来,原因是好多Controller在调用,加一个参数就得改一堆代码!想要在Service的方法中直接获取到HttpServletRequest对象,想象一下,一次请求,Web应用服务器就会分配一个线程去处理。也就是说,在Service方法中获取到的HttpServletRequest对象需要满足:线程内共享,线程间隔离。
这里是RequestContextHolder的源码:是不是和小栗子很像呢~!
主要就是把RequestAttributes对象ThreadLocal化。
然后提供了:
setRequestAttributes();
getRequestAttributes();
等静态方法,来放入或取出ThreadLocal中线程隔离的RequestAttributes。
那么RequestAttributes什么时候被设置的呢?
看看这里。。。FrameworkServlet类。
是servlet的processRequest方法里。那么processRequest又是在哪里调用的呢?
到这里大家明白了吧!在servlet的get,post等等每一次web请求都会被调用绑定!而每一次请求都满足,线程内共享,线程间隔离。。。这不就是ThreadLocal的应用场景么!
Spring中,在管理request作用域的Bean、事务管理、任务调度、AOP等模块都出现了ThreadLocal的身影,起着关键的作用。
应用如此成熟,就让我们走进它看看吧!
认识主角之前先来看一下我们熟悉的那一位:Thread类
在Thread类中有个成员变量threadLocals副本集,默认值为null。 threadLocals是ThreadLocalMap类型的变量。
能看出,ThreadLocalMap是一个近似HashMap。(自然具有HashMap的相关特性,比如自动扩增容量等)。
默认每个线程中的这个变量都是null,那么它在什么时候会被定义呢?
让我们回到例子里信息绑定的那一幕。
向实例ThreadLocal变量里绑定线程Thread id。
不由得要点进去(ThreadLocal的set方法)看啦!
我们先来看set方法
1.首先获取了调用的线程。
2.然后用当前线程做参数获取map。
3.可以看到getMap就是让当前线程获得自己的变量threadLocals副本集。
如果getMap(t)不为空,就把当前变量value值set进当先线程的本地内存变量threadLocals里。threadLocals是hashMap结构,key就是当前ThreadLocal的实例的引用。
如果getMap(t)返回空呢?
4.那就说明当前线程第一次调用set方法,那就创建threadLocals。
key还是那个key,value还是那个value。
看完set,我们再看看get吧!
1.获取当前线程。
2.获取当前线程的threadLocals。
3.如果threadLocals不为空,返回相应的value值(说的好轻巧,后面再说它)。
4.如果为空,就设个初期值吧!
下面看看设置初期值。
1.第一步先初期。protected又设即是空,这是要被自己人覆盖的节奏。
2.万一当前线程threadLocals又不为空了呢?那就绑定上。
3.还为空呢?那就创建。
set和get在流程上相信大家都能明白个大概,但至于map内如何实现了set和get呢?其实还是很复杂的,我们留下课题稍后再看吧!
认识上是不是出现点儿曲折迂回。似乎确实不是那么的简单。
其实每个线程的本地变量并不是存放到ThreadLocal实例里面,而是存放到调用线程的threadLocals里面的。ThreadLocal的本地变量是存放在当前线程的内存空间的。每一个Thread都有一个ThreadLocalMap,它以ThreadLocal为键,以属于该线程的资源副本为值。
我们可以这样看ThreadLocal,ThreadLocal是为一组线程维护资源副本的对象,通过它,可以为每一个线程创建资源副本,也可以正确获得属于某一线程的资源副本。
好像有点儿绕?通俗地说吧!
小孩子们(Thread)都有自己的玩具(玩具是幼儿园统一发的相同玩具,但发给孩子手中就成了每个孩子自己的本地副本),这回孩子们得知有个阿姨(ThreadLocal)能帮他们收拾玩具,每当他们要玩玩具时(当前线程)都找自己的阿姨来存放玩具(set)找玩具(get)及收拾玩具,
阿姨给对应孩子的玩具都建立了清单(threadLocals)而且在清单上写上了自己的名字(key),从此孩子们拿着清单再也不怕拿错玩具了。
画张图让调用关系的思路从回清晰明了的道路上。
1.首先它会获取当前线程对象,然后通过getMap()拿到线程ThreadLocalMap.
2.然后将值设入ThreadLocalMap中。
3.ThreadLocal中的数据,也是写入了threadLocals这个Map中,其中key为ThreadLocal当前对象。
4.value则是我们需要的值。
下面回到好轻巧的话题。
回看get方法
Entry是ThreadLocalMap的静态内部类。
它继承了弱引用。
为什么使用弱引用?仿佛又要进入一个深渊。。。
长话短说,如果使用强引用,当ThreadLocal对象的引用被回收了,ThreadLocalMap本身依然还持有ThreadLocal的强引用,如果没有手动删除这个key,则ThreadLocal不会被回收,所以只要当前线程不消亡,ThreadLocalMap引用的那些对象就不会被回收,可以认为这导致Entry内存泄漏。
如果使用弱引用,那指向ThreadLocal对象的引用就有两个:自身的强引用,和ThreadLocalMap中Entry的弱引用。一旦强引用被回收,则指向ThreadLocal的就只有弱引用了,在下次gc的时候,这个ThreadLocal就会被回收。
又绕圈了?这么想一下!
孩子们玩玩具的时间结束了,接下来他们要去上美术课了!可孩子们的玩具清单上还写着阿姨的名字,阿姨不能离开怎么办?幸好清单上还写着,只有玩具课才来找阿姨哦!上完课阿姨就拜拜(remove)啦!玩具课结束后找不到阿姨不要哭哦!找不到阿姨没关系,如果强引用硬是不让阿姨走被线程池(没set就get)复用的话,可能真的要哭了!
再画一张内存图捋一捋思路吧!
上图可以看出,当调用栈的前引用Ref-1被remove后,ThreadLocal就只剩下了Ref-0的弱引用,那么gc便可以回收ThreadLocal。不过value依然是强引用!它很可能会依然存在于内存中,为什么是可能呢?我们一会儿能看到。
说道remove(),又不得不回到例子上来。
回到拦截器的后处理。
这里手动清理了threadLocal,进去看看。
这里!清理强引用。
引用置空,然后就是(下面)整理Entry。
如何通过hashcode计算储存下标并存储我们姑且不论!就挑干的看!这里,它会判断key为空的值清理掉!value被清理,此时当前线程还在哦!
当前线程自己的threadLocals清理还未执行!
getId()是null,弱引用的key的value被清理完毕!
那问题又来了,如果没有执行remove操作,强引用没有被移除,那么key-value依然存在直到线程结束。所以手动执行remove()是很有必要的。那如果没等到remove,threadLocal强引用不存在了怎么办(当然官方建议我们定义为静态,这样不会出现弱引用溢出,但我就不呢?调用栈用完就把强引用弹出了!)?
那么执行get和set会有影响么?
那就得回到留下课题稍后再看的话题了!
这回先看get方法
从这里进入getEntry();
此时是不是应该进这里了呢!
又到了整理Entry这里,自然会清理key为空的value!
我们再看set方法。
这里进入!
计算下标存储的也不看了,直接这里,也有判定!
。。。
如果存在,判断entry里的key与value如果与当前要保存的key与value相同的话就不保存直接返回。如果entry里的key为null的话,就替换为当前要保存的key与value。
可见,在get,set,remove等等操作中。开发者用尽所能地清理弱引用key的value,可谓是煞费苦心!然而如果线程中当Thread后续没有这些操作又如何呢?我们会发现,无论是强引用还是弱引用,在不手动remove ,并且ThreadLocal强引用为null后,又没有get,set操作,value 也是会泄漏(前提是线程还活着)的,不过至少ThreadLocal被回收了!
就好比玩具课阿姨上完课就走(没remove就null)了!如果这时有上别的课的孩子,辅导员还来带(get)他们去上另一个课,顺便告诉只上玩具课的孩子‘阿姨找不到了,你们先放学吧’!但如果孩子都上玩具课,都没有别的课,辅导员不会来,那就只能等到幼儿园放学了,这也回答了上面(为什么是可能存在于内存中)的问题!但至少阿姨跑路了!
好了!这次进入ThreadLocal就浅尝辄止吧!以后咱们再深入。
三、我的初感受
1.ThreadLocal通过线程自身提供的独立副本集来解决并发冲突,从逻辑上实现了线程封闭的方式十分优雅。
2.ThreadLocal对Thread的引用全部通过局部变量完成,而没有一个全局变量。而实际的资源副本则存储在Thread的自身的属性ThreadLocalMap中,ThreadLocal与Thread虽然看似你中有我我中有你,却只是关联一个Thread和其资源副本的桥梁,它既不引用Thread,也不引用ThreadLocalMap。是我觉得最为巧妙之处。
3.另外在ThreadLocalMap中key使用Threadlocal弱引用,并在各个操作当中虽显被动却不辞辛劳地做key的null判定,防止内存溢出也体现了开发者的用心良苦。