您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
ThreadLocal源码学习
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
ThreadLocal源码学习
自猿其说Tech
2022-08-01
IP归属:未知
31120浏览
### 1 背景 作为一只懒懒地程序员,其实我是不太爱看源码的,晦涩、深奥、难懂、耗费时间等等,就觉得不是我这种能力平平地小老百姓能吃得消的,但现实比人强,记得曾经我就被不懂原理的情况下乱用ThreadLocal给毒打了。 犹记得当时在一个JSF服务中的责任链的校验场景中需要在源头处增加一些返回值,并且由于大部分值都是在某几条具体的校验链中已经获取到并且其他链条中也不需要,所以当时考虑用ThreadLocal在整个链条中在做校验的时候就能顺便将需要返回的值进行填充,并在返回的时候获取从而进行最终结果的包装返回,但当时对于ThreadLocal认知比较浅显同时也没考虑到jsf线程池的问题,导致以为一个线程执行完后ThreadLocal中会自动清空,导致在运行的时候发现了一个奇怪的表象:ThreadLocal中的值拿到的是上次调用里存储的值。 好了,废话少说,咱先来看看ThreadLocal到底是个啥,然后再分析当时为啥会出现上述我所说的异常情况。 ### 2 探索ThreadLocal #### 2.1 初识ThreadLocal 其实我对ThreadLocal的认知都是基于身边的同事聊天或少数场景的代码中见到过,但是对于其原理确实一窍不通,只知道它可以用来在同一个线程中起到传递数据的作用,所以在看源码之前我一直以为ThreadLocal就是一个map,key是当前线程,值是任意Object,不同线程之间的数据是隔离非共享的,只有是同一个线程中其存储的数据才是共享的。 #### 2.2 ThreadLocal基本构成 通过查看代码可看到其由一些属性、方法、内部类构成,属性可不必太关注,只需关注我们常用的set()、get()、remove()方法以及其内部类ThreadLocalMap即可,其简略类图结构如下图所示 ![](//img1.jcloudcs.com/developer.jdcloud.com/baa4de53-a196-44e5-9db3-0ffe2a1ee2fa20220801135806.png) 由图中可看出,ThreadLocal有个内部类ThreadLocalMap,而ThreadLocalMap中也有个内部类Entry,Entry类继承了WeakReference类,泛型指向是ThreadLocal本身。 #### 2.3 ThreadLocal常用方法解析 ##### 2.3.1 set方法 ```java public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } ``` 只看以上这些的话,理解起来很简单,其实就是获取当前线程,根据当前线程拿到对应的ThreadLocalMap对象,然后判断其是否为空,空的话需要创建并对设置value的值,非空则直接设置value的值,有map.set(this,value)大概能看出value的值是跟this-->ThreadLocal本身有关联映射的,那到底是怎么映射的呢,我们接下来进行深入解析,首先进入到getMap(t)方法中进行查看,其源码如下 ```java ThreadLocalMap getMap(Thread t) { return t.threadLocals; } ``` 看到代码是不是很惊讶,我了个去去的,这是个啥么咚咚,就这么一行,是的,就是这么一行简简单单地代码,实质是其直接返回了当前线程内的threadLocals变量,那这个threadLocals变量有何说法呢,我们不妨耐心来来继续往下看,首先看其引用方式,其源码如下 ```java /* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null; /* * InheritableThreadLocal values pertaining to this thread. This map is * maintained by the InheritableThreadLocal class. */ ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; ``` 有以上源码中可以看到threadLocals 和 inheritableThreadLocals 这两个变量都是通过ThreadLocal类静态引用指向其内部类ThreadLocalMap ,并且在Thread类初始化方法init也没发现对threadLocals 变量的赋值动作,但是可以看到inheritableThreadLocals 的赋值动作(那这个跟threadLocals 名字很像的变量到底是做什么用的呢,这里可以先留个疑问,后续也会对其进行解析),代码看到这一步的话,那我们对于ThreadLocal的结构认知也更加清晰了,其结构与关系指向可用如下粗图表示 ![](//img1.jcloudcs.com/developer.jdcloud.com/37da365b-6334-4471-bef0-5a9442f89cd420220801135912.png) 既然我们对于ThreadLocal结构的认知越来越清晰,那咱们继续接着往下看,由上述可知,当第一进行set的时候,threadLocals 是null,那就会走createMap(t,value)这个方法,老规矩先铁山拿过来源码看看 ``` void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); } ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); } ``` 其实质就是new一个ThreadLocalMap,并对table 属性进行初始化,计算出value对应的索引i并设置进去,注意:这里Entry中的映射key为ThreadLocal本身,同时size设为1,并设置扩容因子的值,这个方法比较容易理解,就不多说了,接下来咱们一起来研究研究这个map.set(this,value)方法吧,先贴源码 ```java private void set(ThreadLocal key, Object value) { // We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal k = e.get(); if (k == key) { e.value = value; return; } //k为空说明已经被回收掉了,需要对table进行清理以及rehash动作,最后重新设置value到table中 if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); } ``` 此方法也是先进行计算出要进table的索引i值,然后再将value包装成Entry实体设置进rable中,这里有个小细节就是hash冲突的问题,就是这里拿到了索引i的时候并不是直接采用,而且通过for循环进行了一些处理,大家看到这里是不是感觉有点熟悉,怎么感觉像跟HashMap中的处理比较类似,但是这里其实是跟HashMap中的处理是不同的,HashMap是链式地址法来解决hash冲突的(数组+链表),而ThreadLocal是通过开放地址法来解决hash冲突的(数组i索引往后继续寻找,直到找到新的空位置),具体赛值情况大概归为三种场景 整个数组中并不存在与当前入参中的key是一样的:直接new一个到对应空位置i的索引下,如果需要rehash则进行rehash(比如打到了扩容标准) - 数组中存在key与当前入参key一样并且不为空:直接覆盖原有value并返回 - 数组中存在key与当前入参key一样并且为空:说明已经被回收了,需要进行对无效实体进行清理并进行rehash操作,同时将新值设置进去-->我们重点来看这个方法是怎么来实现的(replaceStaleEntry(key, value, i);) ```java private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; // Back up to check for prior stale entry in current run. // We clean out whole runs at a time to avoid continual // incremental rehashing due to garbage collector freeing // up refs in bunches (i.e., whenever the collector runs). int slotToExpunge = staleSlot; for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e.get() == null) slotToExpunge = i; // Find either the key or trailing null slot of run, whichever // occurs first for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); // If we find key, then we need to swap it // with the stale entry to maintain hash table order. // The newly stale slot, or any other stale slot // encountered above it, can then be sent to expungeStaleEntry // to remove or rehash all of the other entries in run. if (k == key) { e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; // Start expunge at preceding stale entry if it exists if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } // If we didn't find stale entry on backward scan, the // first stale entry seen while scanning for key is the // first still present in the run. if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } // If key not found, put new entry in stale slot tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); // If there are any other stale entries in run, expunge them if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); } ``` 1)这个方法整体逻辑实现可以这么来描述(里边的了一些私有方法就不贴源码了,有兴趣的可自行研究下): 2)从当前索引i位置向前遍历,找到第一个key为空的位置并记录下来: slotToExpunge(注意此块有可能还会是i本身,可以自己看下prevIndex方法)从当前索引位置向后遍历,若其k与当前key相等,则将当前索引出位置与遍历到的key相等的位置进行互换,并从slotToExpunge位置开始清除过期的数据,并结束 - 向后遍历,将key为null的清除掉,并将数组size更新 - 若key不为null,则判断新计算的h值与当前索引是否相等,若不相等则将当前位置i处清除掉(其值为e),并从h处向后遍历,直到找到位置为null,然后将e赋值到这里 - i和ii步骤皆为expungeStaleEntry方法的实现,其最后返回对应位置为null的索引位置,暂即为ri,后续会用到 - 上述的索引ri和数组长度作为此方法的入参:cleanSomeSlots,继续进行清除过期数据 - ri处向后遍历,找到key为空的位置,并从此位置开始,调用expungeStaleEntry方法继续进行清除 3)如果遍历时未达到返回条件,则将当前位置i处插入一个新的Entry节点(要设置的value和入参k) 4)如果运行过程中还产生了其他的过期数据则继续进行清除 以上这些就是一个完整的set方法流程了,接下来咱们来看看get方法流程,画个简略存储使用图,如下 ![](//img1.jcloudcs.com/developer.jdcloud.com/4c1a11e0-f6db-48fc-bf5c-d4b5a177671a20220801140200.png) ##### 2.3.2 get方法 ```java public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) return (T)e.value; } return setInitialValue(); } ``` get方法相对于set方法来说比较简单一些,这里重点看一下这个方法(map.getEntry(this))就好,其他的可自行阅读 ```java private Entry getEntry(ThreadLocal key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); } ``` 这个方法也比较好理解,我们来看下getEntryAfterMiss方法的实现是怎样的 ```java private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal k = e.get(); if (k == key) return e; if (k == null) expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; } ``` 我们可看到遍历时有个expungeStaleEntry调用,这个方法之前在set方法中已经描述过其实现逻辑了,而上述这块整体逻辑也就比价容易理解了:i处向后遍历,k==key则返回,k为null时进行过期数据清除,直到遍历到索引位置处为null时为止 ##### 2.3.3 remove方法 ```java public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); } private void remove(ThreadLocal key) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } } } public void clear() { this.referent = null; } ``` 这个整体逻辑比较容易理解,而且里边一些重要方法在set和get中均有过描述了,这里我们就看下这两行来了解其是怎么清除数据的e.clear();expungeStaleEntry(i); 我们看到这个clear就是将对应的key设置为null,然后再调用expungeStaleEntry方法进行过期数据清除 到这里对于ThreadLocal我们应该有个比较清晰的认识了,那我们来回归下文章开始处所提到的那个奇怪现象是怎么出现的:ThreadLocal中的值拿到的是上次调用里存储的值 #### 2.4 解析奇怪现象本质 在说这个奇怪现象的时候,首先先提一下当时我的使用方式:就是在set的地方进行对应值的set(4种类型),最后在所有链条结束时再通过get取出来,但是最后并未在finally处进行remove(以为反正每次都是新的线程进来就没进行remove,待线程结束后交给GC自动回收) 而我们当时提供出的服务是jsf模式提供的,jsf的线程池方式默认为缓存模式,这时候很有可能请求A来时启动了jsf线程K,当调用完成后,请求C来的时候jsf线程K还未销毁(未达到最大个数或空闲时间未达到)可继续进行复用,这时候比如A场景时设置进去了4种类型的值,然后B场景只是设置了其中2种类型的值,这时候在最后组装数据的时候B场景get时4种场景都能拿到值,然后就造成了上述所说的奇怪现象了 ### 3 结语 对于一些不明其理的事物还是不要想当然的乱用,最起码也要大概懂得其基本使用场景以及使用规范才可放心使用,否则出现一些无法理解的问题就无从下手,并且建议使用的时候使用这种方式:private static final 修飾. 同时上述所讲的提到了线程池和inheritableThreadLocals 变量,那如果这种线程池或者父子线程这种类似场景ThreadLocal又该使用呢,同时在看源码的时候也发现Entry里的key是弱引用的以及set、get、remove中都出现了key为null的处理逻辑,那这又是为什么呢,又会不会有内存泄漏的嫌疑,咱们下回分解。 ------------ ###### 自猿其说Tech-JDL京东物流技术与数据智能部 ###### 作者:张小龙
原创文章,需联系作者,授权转载
上一篇:测试角色在项目各阶段的项目管理tips
下一篇:飞码LowCode前端技术(一)
自猿其说Tech
文章数
426
阅读量
2149964
作者其他文章
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
阅读量
2149964
作者其他文章
01
深入JDK中的Optional
01
Taro小程序跨端开发入门实战
01
Flutter For Web实践
01
配运基础数据缓存瘦身实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号