您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
一只菜狗的ThreadLocal源码之旅
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
一只菜狗的ThreadLocal源码之旅
自猿其说Tech
2021-10-13
IP归属:未知
33760浏览
计算机编程
项目开发中少不了使用 ThreadLocal来操作存储变量,非常方便,可以省去大量的变量在方法之间的传递。而且线程之间数据隔离。这么厉害的一个东西,实现起来一定非常的复杂。这个观点是我一开始接触到ThreadLocal时的真实想法。但是,当我打开jdk源码看到这个ThreadLocal实现的时候,我惊了。连注释加起来722行。且每个方法都非常简洁,这就让我有了读下去的信心。 ### 1 ThreadLocal源码解析 #### 1.1 ThreadLocal总体结构 ![](//img1.jcloudcs.com/developer.jdcloud.com/b0662394-2485-42f0-b7a6-f6fe6ca7f97b20211013145925.png) 打开idea,ctrl+o。ThreadLocal结构图映入我们眼帘,我们可以看到,ThreadLocal本身只有一个无参构造,且公开的方法除去无参构造也只有4个他们分别是set(T);get();remove();和withInitial(Supplier<? extends T>); 这四个方法想必大家已经用的非常熟悉了,我们暂且先不去分析。接着往下看。 #### 1.2 ThreadLocalMap 个人理解,ThreadLocal的精髓或者说巧妙之处完全体现在ThreadLocalMap之中,搞明白了ThreadLocalMap也就理解了百分之90的ThreadLocal的源码。下面我们来着重的分析一下这个ThreadLocalMap。从命名上我们可以看出这个ThreadLocalMap的本身存储结构是一个map,既然是map,就少不了自己的key和value。 ##### 1.2.1 Entry节点 我们来看一下,ThreadLocalMap节点的Entry ![](//img1.jcloudcs.com/developer.jdcloud.com/549a31df-59a1-49fb-b2f2-13a61598fde620211013145957.png) 它继承了WeakReference类,定义了一个类型为Object的value,用于存放塞到ThreadLocal里的值。 **那么问题来了。为什么要使用弱引用?** 我们先来假设一下,如果不适用弱引用会如何?如果没有使用弱引用,而是使用了一般key-value的形式来存储,只要线程没有被销毁,那么这个Entry节点永远不会被当做垃圾回收掉。而使用了弱引用,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。当某个ThreadLocal已经没有强引用可达,则随着它被垃圾回收,在ThreadLocalMap里对应的Entry的键值会失效。这就是为什么使用弱引用的原因。 ##### 1.2.2 ThreadLocalMap分析 ![](//img1.jcloudcs.com/developer.jdcloud.com/a173205e-bc8a-47df-a627-26c13824136b20211013150021.png) 从ThreadLocalMap的构造函数中我们可以看出,它做了这么几件事 1. 初始化了一个大小为16的Entry数组 1. 搞了一个Entry数组下标(这步很重要一开始没看懂就接着往下看了) 1. 初始化了一个Entry对象,将其放到了1中初始化的Entry数组中 1. size设置为1 1. set了一个阈值 然后就没有了,到这里为止我有几个疑问,这个数组下标为啥这么搞?重复了咋整呢?这不冲突了吗? ![](//img1.jcloudcs.com/developer.jdcloud.com/c2b8736e-2e0a-4651-9de2-76150607f13620211013150122.png) 为啥要按照这个0x61c88647这个数累加?带着这些疑问,我写了一个测试类。 ![](//img1.jcloudcs.com/developer.jdcloud.com/af985593-1d5a-431a-bac8-0c22f59a161420211013150134.png) 从结果可以看出,竟然没有冲突,很均匀。经过查阅资料,0x61c88647是斐波那契散列乘数,它的优点是通过它散列(hash)出来的结果分布会比较均匀,可以很大程度上避免hash冲突。 所以这里,我们可以得出这样的结论 对于对于某一ThreadLocal来讲,他的索引值i是确定的,在不同线程之间访问时访问的是不同的table数组的同一位置即都为table[i]。 对于同一线程的不同ThreadLocal来讲,这些ThreadLocal实例共享一个table数组,然后每个ThreadLocal实例在table中的索引i是不同的。 #### 1.3 ThreadLocal的set方法 视线转回我们ThreadLocal的set方法。 ![](//img1.jcloudcs.com/developer.jdcloud.com/e50780eb-8883-4aa8-969c-ec69731ea99420211013150204.png) 这里我们可以看出ThreadLocal的set方法其实就是调用对应的ThreadLocalMap的set方法如果ThreadLocalMap不为空的情况下。 ![](//img1.jcloudcs.com/developer.jdcloud.com/51a4def2-4954-4bbc-b4f8-686b4475a28620211013150221.png) set方法中主要做了这几件事 1. 遍历Entry数组,存在当前ThreadLocal则更新值。 1. 遍历完之后如果没有当前ThreadLocal则创建。 1. 如果达到阈值或发现任何失效的Entry节点则调用清理方法。 #### 1.4 ThreadLocal的get方法 ![](//img1.jcloudcs.com/developer.jdcloud.com/5820f251-7b9a-49cc-aa54-3b194090719d20211013150246.png) 从源码我们可以看出ThreadLocal的get方法调用了ThreadLocalMap的get方法下面我们就着重看一下ThreadLocalMap的getEntry方法 ![](//img1.jcloudcs.com/developer.jdcloud.com/ed65c0fd-bdb1-49f0-82b3-601a0fe3507f20211013150302.png) ThreadLocalMap的getEntry方法先去看在当前Entry数组中是否可以命中当前ThreadLocal如果命中直接返回Entry如果没有的话则执行getEntryAfterMiss方法,我们再进一步看下一这个getEntryAfterMiss方法 ![](//img1.jcloudcs.com/developer.jdcloud.com/03eed6e8-49e9-4a58-ac3c-4a135b71363320211013150314.png) getEntryAfterMiss方法中如果没有找到对应的Entry则循环遍历Entry数组,如果找则返回,如果key为空也就是已经失效,则调用expungeStaleEntry方法去清理当前的槽点开始至最终末位槽点中已经 失效的Entry,expungeStaleEntry的具体实现方法如下 ![](//img1.jcloudcs.com/developer.jdcloud.com/14209ded-a471-4809-b11f-6755503e122020211013150327.png) 到此我们可以小结一下。 1. getEntry方法如果算出来的下标能够直接命中ThreadLocal,则直接返回。 1. 如果没有直接命中则调用getEntryAfterMiss发放,此方法会循环遍历对应的Entry数组,过程中遇到失效的ThreadLocal直接清理,如果找到了ThreadLocal则返回对应的结果 1. 没有找到对应的key,返回null #### 1.5 ThreadLocal的remove方法 ThreadLocal的remove方法其实还是调用了本ThreadLocal中ThreadLocalMap的remove方法下图为ThreadLocalMap的remove方法 ![](//img1.jcloudcs.com/developer.jdcloud.com/8df85efd-d356-49c7-8571-c1d8a9c230ba20211013150356.png) ThreadLocalMap的remove方法主要做了这么几件事 1. 找到ThreadLocal所在的Entry然后直接将其引用指向null 1. 调用我们上面分析过的expungeStaleEntry来进行清理 ### 2 ThreadLocal使用中的注意事项 说到ThreadLocal很多人第一反应就是他会引起内存泄漏的问题。但是当我们一步一步的看完ThreadLocal的源码我们会发现,ThreadLocal中设计了一套自清理机制,这套自清理机制可以确保,ThreadLocal不会出现因为不被回收的问题,但是前提是 !你要在其实用完成之后调用remove去清除呀! 假设你没有调用remove方法,再次调用set或get方法也能对应的清理掉无效的对象。从而使得对象被垃圾收集器收集。所以只要使用完对应的ThreadLocal之后能够及时的remove方法。是不会出现所谓的内存泄漏的问题的。所以,大家可以放心使用。 ### 3 总结 通过阅读ThreadLocal源码,我们了解了,ThreadLocal的主要功能实现以来于其中的ThreadLocalMap,我们也了解了ThreadLocalMap大致的实现原理,最后谈了一下我个人对ThreadLocal的一些使用事项。以及我个人对ThreadLocal可能会引起内存泄漏这个问题的观点和看法。欢迎各位大佬前来交流讨论。 ------------ ###### 自猿其说Tech-JDL京东物流技术发展部 ###### 作者:客户服务技术部 王子源
原创文章,需联系作者,授权转载
上一篇:浅析@Transactional注解原理及使用
下一篇:简述iOS开发中常用的编程思想
相关文章
Taro小程序跨端开发入门实战
Flutter For Web实践
配运基础数据缓存瘦身实践
自猿其说Tech
文章数
426
阅读量
2149963
作者其他文章
01
深入JDK中的Optional
本文将从Optional所解决的问题开始,逐层解剖,由浅入深,文中会出现Optioanl方法之间的对比,实践,误用情况分析,优缺点等。与大家一起,对这项Java8中的新特性,进行理解和深入。
01
Taro小程序跨端开发入门实战
为了让小程序开发更简单,更高效,我们采用 Taro 作为首选框架,我们将使用 Taro 的实践经验整理了出来,主要内容围绕着什么是 Taro,为什么用 Taro,以及 Taro 如何使用(正确使用的姿势),还有 Taro 背后的一些设计思想来进行展开,让大家能够对 Taro 有个完整的认识。
01
Flutter For Web实践
Flutter For Web 已经发布一年多时间,它的发布意味着我们可以真正地使用一套代码、一套资源部署整个大前端系统(包括:iOS、Android、Web)。渠道研发组经过一段时间的探索,使用Flutter For Web技术开发了移动端可视化编程平台—Flutter乐高,在这里希望和大家分享下使用Flutter For Web实践过程和踩坑实践
01
配运基础数据缓存瘦身实践
在基础数据的常规能力当中,数据的存取是最基础也是最重要的能力,为了整体提高数据的读取能力,缓存技术在基础数据的场景中得到了广泛的使用,下面会重点展示一下配运组近期针对数据缓存做的瘦身实践。
自猿其说Tech
文章数
426
阅读量
2149963
作者其他文章
01
深入JDK中的Optional
01
Taro小程序跨端开发入门实战
01
Flutter For Web实践
01
配运基础数据缓存瘦身实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号