threadlocal的应用场合_初识ThreadLocal

一、概括说一句

    ThreadLocal提供了线程本地变量,也就是当创建了一个ThreadLocal变量,那么访问这个变量的每个线程都拥有了这个变量的本地拷贝。多个变量同时操作这个变量时,实际都是在操作自己的本地内存里面的变量,从而避免现成的安全问题。

二、走进看一看

    概括的这句话从字面上看起来很容易理解,但是真正理解似乎并不是那么容易。

    先来一个小栗子。

0dcb814da5fb5f48686d880c147da375.png

    一个简单的springBoot工程。

21f69268163eb23f05a1db8fd4c8822e.png

    过滤器:获取当前线程id,并添加绑定。

a31b9610bb9394fc75bad72c6fb4fd9f.png

    拦截器:前处理和后处理分别打印绑定的数据。

20a929f2f1f85d0cd60ecc3151479d77.png

    主角登场,绑定,获取和移除都有它来处理。

9dda1122867170f7bda3e78d72eac422.png

    终点站:控制器也输出一下 绑定的 线程id。

    接下来执行一下吧!

bd6dfda2e5422ba56bd1f4aa5751e9ec.png

    过滤器,前处理,控制器,后处理及最后移除绑定!例子很简单,相信大家都熟悉,在权限校验,用户信息获取等session场合,似乎同学们都没有被并发下的安全问题发过愁,毕竟ThreadLocal实现了线程封闭嘛!

  现实案例很多,举一个SpringWeb中的案例:直接获取HttpServletRequest对象,让我的小栗子能站在巨人的肩膀上。

  在Service方法中使用HttpServletRequest的API,但是又不想把HttpServletRequest对象当作这个Service方法的参数传过来,原因是好多Controller在调用,加一个参数就得改一堆代码!想要在Service的方法中直接获取到HttpServletRequest对象,想象一下,一次请求,Web应用服务器就会分配一个线程去处理。也就是说,在Service方法中获取到的HttpServletRequest对象需要满足:线程内共享,线程间隔离。

    这里是RequestContextHolder的源码:是不是和小栗子很像呢~!

270a75fa138277e6250efac12f43d2e2.png

   主要就是把RequestAttributes对象ThreadLocal化。

   然后提供了:

           setRequestAttributes();

           getRequestAttributes();

 等静态方法,来放入或取出ThreadLocal中线程隔离的RequestAttributes。

   那么RequestAttributes什么时候被设置的呢?

   看看这里。。。FrameworkServlet类。

18a8aee2ec8eb6233cccc91071d94776.png

     是servlet的processRequest方法里。那么processRequest又是在哪里调用的呢?

36a865ab0e38e0cc10a6b69f5df5d24d.png

     到这里大家明白了吧!在servlet的get,post等等每一次web请求都会被调用绑定!而每一次请求都满足,线程内共享,线程间隔离。。。这不就是ThreadLocal的应用场景么!

     Spring中,在管理request作用域的Bean、事务管理、任务调度、AOP等模块都出现了ThreadLocal的身影,起着关键的作用。

    应用如此成熟,就让我们走进它看看吧!

    认识主角之前先来看一下我们熟悉的那一位:Thread类

82ce9e3eb23bd690ea53aff0c66baafb.png

    在Thread类中有个成员变量threadLocals副本集,默认值为null。 threadLocals是ThreadLocalMap类型的变量。

2c3f3cc3edd35d7db217b1b46ec1fbd6.png

   能看出,ThreadLocalMap是一个近似HashMap。(自然具有HashMap的相关特性,比如自动扩增容量等)。

    默认每个线程中的这个变量都是null,那么它在什么时候会被定义呢?

    让我们回到例子里信息绑定的那一幕。

c6b53cec5df460735077d76a5ef434fd.png

    向实例ThreadLocal变量里绑定线程Thread id。

f9ecb24a4c7bbcbb1286b56c955cda86.png

    不由得要点进去(ThreadLocal的set方法)看啦!

    我们先来看set方法

    1.首先获取了调用的线程。

01039f7d01896646772222572752a72e.png

   2.然后用当前线程做参数获取map。

63bb54e2e6ed2debb90b48fa92ad74a1.png

   3.可以看到getMap就是让当前线程获得自己的变量threadLocals副本集。

   如果getMap(t)不为空,就把当前变量value值set进当先线程的本地内存变量threadLocals里。threadLocals是hashMap结构,key就是当前ThreadLocal的实例的引用。

   如果getMap(t)返回空呢?

   4.那就说明当前线程第一次调用set方法,那就创建threadLocals。

8a60817370ca91708be830e61c8531a9.png

   key还是那个key,value还是那个value。

   看完set,我们再看看get吧!

78987ff7514e754c5e69c2553d2b1226.png

    1.获取当前线程。

    2.获取当前线程的threadLocals。

    3.如果threadLocals不为空,返回相应的value值(说的好轻巧,后面再说它)。

    4.如果为空,就设个初期值吧!

    下面看看设置初期值。

5eda139d3c1fca4707287a1d483586f2.png

    1.第一步先初期。protected又设即是空,这是要被自己人覆盖的节奏。

9b8e04f1f984bdc3441b534725dc50e4.png

    2.万一当前线程threadLocals又不为空了呢?那就绑定上。

    3.还为空呢?那就创建。

    set和get在流程上相信大家都能明白个大概,但至于map内如何实现了set和get呢?其实还是很复杂的,我们留下课题稍后再看吧!

    认识上是不是出现点儿曲折迂回。似乎确实不是那么的简单。

    其实每个线程的本地变量并不是存放到ThreadLocal实例里面,而是存放到调用线程的threadLocals里面的。ThreadLocal的本地变量是存放在当前线程的内存空间的。每一个Thread都有一个ThreadLocalMap,它以ThreadLocal为键,以属于该线程的资源副本为值。

    我们可以这样看ThreadLocal,ThreadLocal是为一组线程维护资源副本的对象,通过它,可以为每一个线程创建资源副本,也可以正确获得属于某一线程的资源副本。

    好像有点儿绕?通俗地说吧!

    小孩子们(Thread)都有自己的玩具(玩具是幼儿园统一发的相同玩具,但发给孩子手中就成了每个孩子自己的本地副本),这回孩子们得知有个阿姨(ThreadLocal)能帮他们收拾玩具,每当他们要玩玩具时(当前线程)都找自己的阿姨来存放玩具(set)找玩具(get)及收拾玩具,

阿姨给对应孩子的玩具都建立了清单(threadLocals)而且在清单上写上了自己的名字(key),从此孩子们拿着清单再也不怕拿错玩具了。

    画张图让调用关系的思路从回清晰明了的道路上。

0a2d5a8e6e03dd1ce122b39b44a19051.png

   1.首先它会获取当前线程对象,然后通过getMap()拿到线程ThreadLocalMap.

    2.然后将值设入ThreadLocalMap中。

   3.ThreadLocal中的数据,也是写入了threadLocals这个Map中,其中key为ThreadLocal当前对象。

   4.value则是我们需要的值。

    下面回到好轻巧的话题。

    回看get方法

7e5cf7298d97709f0bd3973e56d1d9af.png

    Entry是ThreadLocalMap的静态内部类。

fd08e6b932fa7fb18ab786e5a462f08e.png

    它继承了弱引用。

    为什么使用弱引用?仿佛又要进入一个深渊。。。

    长话短说,如果使用强引用,当ThreadLocal对象的引用被回收了,ThreadLocalMap本身依然还持有ThreadLocal的强引用,如果没有手动删除这个key,则ThreadLocal不会被回收,所以只要当前线程不消亡,ThreadLocalMap引用的那些对象就不会被回收,可以认为这导致Entry内存泄漏。

     如果使用弱引用,那指向ThreadLocal对象的引用就有两个:自身的强引用,和ThreadLocalMap中Entry的弱引用。一旦强引用被回收,则指向ThreadLocal的就只有弱引用了,在下次gc的时候,这个ThreadLocal就会被回收。

    又绕圈了?这么想一下!

    孩子们玩玩具的时间结束了,接下来他们要去上美术课了!可孩子们的玩具清单上还写着阿姨的名字,阿姨不能离开怎么办?幸好清单上还写着,只有玩具课才来找阿姨哦!上完课阿姨就拜拜(remove)啦!玩具课结束后找不到阿姨不要哭哦!找不到阿姨没关系,如果强引用硬是不让阿姨走被线程池(没set就get)复用的话,可能真的要哭了!

    再画一张内存图捋一捋思路吧!

1b4a13690f07b8c82fe35611e21ff24d.png

     上图可以看出,当调用栈的前引用Ref-1被remove后,ThreadLocal就只剩下了Ref-0的弱引用,那么gc便可以回收ThreadLocal。不过value依然是强引用!它很可能会依然存在于内存中,为什么是可能呢?我们一会儿能看到。

    说道remove(),又不得不回到例子上来。

    回到拦截器的后处理。

fafe4487c65f9f3debafc7e3c593f633.png

    这里手动清理了threadLocal,进去看看。

67d6852a6ad14995a3323147a332aaad.png

    这里!清理强引用。

92bdf9f5efcc420c0c5b2b9a1f8b9bb5.png

    引用置空,然后就是(下面)整理Entry。

f237bc5cb73ed5c7151463b01a39d710.png

    如何通过hashcode计算储存下标并存储我们姑且不论!就挑干的看!这里,它会判断key为空的值清理掉!value被清理,此时当前线程还在哦!

1434e29e6276ebebfbf59c2a6ebb9c96.png

    当前线程自己的threadLocals清理还未执行!

2011ca2bf19e6ec0040503e1f581f23c.png

    getId()是null,弱引用的key的value被清理完毕!

    那问题又来了,如果没有执行remove操作,强引用没有被移除,那么key-value依然存在直到线程结束。所以手动执行remove()是很有必要的。那如果没等到remove,threadLocal强引用不存在了怎么办(当然官方建议我们定义为静态,这样不会出现弱引用溢出,但我就不呢?调用栈用完就把强引用弹出了!)?

    那么执行get和set会有影响么?

    那就得回到留下课题稍后再看的话题了!

    这回先看get方法

1e5b845c8d68fe9cef9bb05b5f7d3a18.png

    从这里进入getEntry();

c32d6e7064e6a374cb6b19bfc9bdab1e.png

    此时是不是应该进这里了呢!

85a47425fc7e52a655403274bf810ca6.png

    又到了整理Entry这里,自然会清理key为空的value!

    我们再看set方法。

01bd1a6cf248fbdd7af238bdebeb4ea3.png

    这里进入!

4db242d649a445cd36dd3a6b3f568948.png

    计算下标存储的也不看了,直接这里,也有判定!

904d77981320b995b6be5041fb310869.png

    。。。

7a964d50c1ff498d05d19c71840c7788.png

    如果存在,判断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判定,防止内存溢出也体现了开发者的用心良苦。

f325a5ed3062bd8396eba6a52d620f55.png

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值