Dubbo中存在一些优化设计,这些设计具备必定的参考价值,这里调研下 InternalThreadLocal 的优化设计。apache
如下内容的章节为:数组
- ThreadLocal的介绍
- InternalThreadLocal的介绍
- InternalThreadLocal和ThreadLocal的对比和使用范围
- 垃圾回收的考虑
1.ThreadLocal的介绍
查看org.apache.dubbo.common.threadlocal.InternalThreadLocal 的时候,会发现这里有一个特殊的设计,说是参考Netty的优化设计,对ThreadLocal进行了优化。安全
ThreadLocal咱们知道能够经过这个类在一个线程内共享线程级变量,这里简单地介绍下线程级变量的实现原理。以下图所示:多线程

假设有一个ThreadLocal<User> currentLocalUser 的线程级变量,那么这个线程级变量的存储逻辑为:函数
- 获取当前的执行Thread,获取其内部变量threadLocals,这是一个Map结构,可是这个Map是一个自定义的Map,与咱们常用的TreeMap和HashMap不是一回事。
- threadLocals的Map结构,内部由Entry组成,能够简单理解为一个Pair<Key,Value>的键值对。Key能够理解为ThreadLocal对象(其实还有弱引用。后面再讲),Value能够理解为ThreadLocal的泛型里表示的线程级变量。
- 在键值对的基础上,如何实现Map查询?threadLocals是一个Hash表的结构,内部是一个固定长度Entry数组,Key经过计算hash,映射到一个Entry的index上,实现定位。
- 提到hash表的实现,就必须考虑到如何解决碰撞问题??JDK的HashMap采用的是拉链法和红黑树,这里的作法是开放地址法,直接在计算出index上+1,再次尝试插入,看是否碰撞,直至成功。
- 若是采用开放地址法,那么必须考虑扩容问题,不然若是map塞满了,一直用开放地址法,也找不到空闲的位置了,这里采用的方式是有一个阈值,默认2/3的负载因子,超过就*2进行扩容。
- threadLocals采用的是类hashmap的实现方式,那么其hash性能就很重要,这里有一个有趣的数字0x61c88647,它的值是2的32次方乘以0.618黄金比例。每次有set一个新key的时候,内部有个全局计算器,加上0x61c88647以后的值,除之内部的Entry数组的长度,做为hash过的index的值。Entry数组的长度是2的次方,每次扩容也是乘以2。你们能够查一下资料,0x61c88647是一个性能比较好的数值,在不停累加,并且取2的整数次方的余数后,离散性能很好,这里再也不赘述,网上不少文章,我也不能彻底推理出来。
总结一下,若是有一个ThreadLocal的线程级变量要初始化并插入,那么其简单步骤为:性能
- 经过当前的线程,获取其内部的threadLocals的Map结构。
- 这个ThreadLocal的内部变量threadLocalHashCode初始化,每次在全局计数器的基础上增长0x61c88647这个魔数,做为threadLocalHashCode的值。
- 计算出的threadLocalHashCode对当前的Map里的Entry数组的长度取余数,就获得具体的index下标的存储位置。固然了,若是扩容了,会有resize来处理,这里只讨论简单状况,不考虑扩容和hash碰撞的问题。
- 建立出一个Entry对象,能够简单理解为Pair<WeakReference<ThreadLocal>,User> 这么一个键值对,Entry就放在上一步计算的index的Entry数组里。
- 线程级变量插入完毕,继续其余的业务。
- 这里会发现,对这个Map的操做没有加锁操做,是由于这个Map原本就是当前的线程的内部变量,只有当前线程能够操做内部的Map,每一个线程都操做本身的map,故没有线程间共享抢占的问题。
若是想要获取currentLocalUser当前的线程级变量里的user,其简单步骤为:优化
- 经过当前的线程,获取其内部的threadLocals的Map结构。
- 经过currentLocalUser这个ThreadLocal,获得其内部变量threadLocalHashCode,除以Map结构的Entry数组的长度,获取其index,获取一个Entry对象,里面是Pair<WeakReference<ThreadLocal>,User>这种结构。
- 注意:这是hash版本的Map,那么可能存在碰撞,因此会判断WeakReference<ThreadLocal>里的ThreadLocal对象,是否是currentLocalUser这个变量,若是不是,+1计算下一个index,直至相等,此时取出User对象
- 此时的这个User对象就是线程级变量。若是这个User对象没有经过其余的引用与其余的线程分享,那么User就是线程安全的。
总体总结:线程
经过自增一个特殊数字的方式,再对数组取余,实现了hash元素定位,在冲突的时候,采用+1的方式,再次定位,直至找到一个空闲的槽位,或者在查找的时候,直至吵到等于本对象的槽位。你可能意识到,这种hash的方式确定会存在碰撞,并且碰撞后,经过自增的方式重定位以后,极端状况下存在线性时间复杂度的开销,一直有冲突,须要找到本身的槽位。设计
2.InternalThreadLocal的介绍
Dubbo存在一种InternalThreadLocal的优化对象,来模拟ThreadLocal的操做,来优化性能,他的基本结构和JDK本身的方案差很少,只是在hash方案上有些出入。对象
上面提到,ThreadLocal与Thread线程里的内部变量threadLocals有关系,那么Thread线程与InternalThreadLocal压根不要紧,因此须要InternalThread的配合,InternalThread继承了Thread对象,内部增长了一个threadLocalMap的内部Map对象,来存在InternalThreadLocal和对象的线程级变量的映射关系。InternalThreadLocal和InternalThread的关系,与ThreadLocal和Thread的关系几乎同样,这里再也不画图。
重点在InternalThread内部的map的hash方案上,这里的hash方案也是index的自增,不过不是自增0x61c88647,而是直接加1,每次计算出的index,就是内部的Map的Entry数组的下标。直接定位,不会出现冲突。由于:每一个InternalThreadLocal对象的index的值,都是自增的,是不会出现冲突的。
那么问题又来了:若是每次都自增,随着程序的运行,这个index会不会愈来愈大,内部的Entry数组愈来愈大,最后OOM?
答案是:正常状况下不会。经过检索Dubbo的源码,会发现全部的InternalThreadLocal的使用,都是static的使用,因此InternalThreadLocal实例的个数是肯定的,其index也不会无限制的增长。
总体总结:
InternalThreadLocal做为dubbo内部的高性能的线程级变量的实现,虽然表面上是Map的名字,其实是数组的访问速度。因此在多线程频繁访问线程级变量的状况下,必定速度很快。可是也有局限性,最好做为dubbo的内部变量使用,外部不要直接使用。若是外部确实要使用,也要使用static的方式,若是伴随着业务代码,一直在new InternalThreadLocal,会形成内部的index一直累加,致使Map内部的数组也一直膨胀,直到OOM。就算不OOM,内部也会触发逻辑:

3.InternalThreadLocal和ThreadLocal的对比和使用范围
|
优点 |
劣势 |
InternalTreadLocal |
速度高,数组访问级别的速度。由于没有hash碰撞的问题,性能一直是O(1) |
在dubbo内部使用,最好不要在本身的代码中使用 确实要使用,使用static的方式,防止OOM |
ThreadLocal |
通用性强 |
在没有碰撞的状况下,访问速度是O(1),在最坏的状况下,访问速度接近于O(map的容量) |
4.垃圾回收的考虑
通过简单分析这个结构后,这个线程级共享变量机制的一个重要问题是垃圾回收。Java不是自动垃圾回收么,为何要考虑垃圾回收?
由于这些线程级变量是跟线程有关的,而在GC的时候,JVM扫描变量可达性的时候,部分可达性分析会以Thread为根开始扫描,此部分能够搜索GC ROOT的概念。与GC ROOT有强引用的内存是不会回收的。
先分析ThreadLocal的垃圾回收场景:
- 一个正在执行的线程确定是不能够回收的,那么Thread内部的threadLocals的这个Map结构确定也不会回收的,这是一个强引用StrongReference;
- map结构的内部,主要分为3部分:Entry结构体(至关于Pair<Key,Value>结构),Key部分就是WeakReference<ThreadLocal>,value部分就是ThreadLocal的泛型内表达的值。
- Entry部分回收不计,这部分的回收取决于内部的keyValue的回收,在回收的时候,一并回收。
- Key部分很特殊,是WeakReference弱引用,弱引用的部分,若是GC的时候,内部的ThreadLocal会被回收。可是,业务逻辑执行中,是不会被回收的,分为两种状况:若是你声明的ThreadLocal是static的,那么存在一个永生带到这个static的ThreadLocal实例的强引用,而永生带属于GC ROOT的一部分,跟Thread做为GC ROOT的待遇同样。若是ThreadLocal是new的临时变量,在业务逻辑执行中,JVM的栈上会对这个临时变量有一个强应用,栈区也是GC ROOT的一部分,因此在业务过程当中,WeakReference即便想回收ThreadLocal也不会真的回收。
- 可是若是业务逻辑执行完了,并且ThreadLocal是new出来的临时变量,那么其ThreadLocal的实例可能被回收,此时,WeakReference<ThreadLocal>的Key部分,若是查询,会发现存储的ThreadLocal已经为null。此时问题来了:Entry做为Map内部的数组的一部分,是一个强引用,而Value部分是Entry的一个强引用,此时Entry和Value都没法回收,岂不是形成了内存泄漏的问题,可是实际上正常使用是不会内存泄漏的,由于ThreadLocal的set和get方法内部自带了垃圾回收,若是发现key部分已经回收了,就把value置为null,entry置为null,帮助JVM回收。
- ThreadLocal的回收部分实际更复杂,能够搜索【ThreadLocal set get】检索文章查看细节,更复杂的地方是:插入的时候,有hash碰撞,采用了index+1的开放地址法处理冲突,若是中间有个Entry回收了,须要把后面的有效的Entry向前移动,不然后面的节点在在get的时候,用index+1的方式进行探测的时候,会增长额外的代码复杂度和存储空间消耗。
再分析InternalThreadLocal的垃圾回收场景:
答案是没有这么复杂的垃圾回收,由于没有垃圾产生。
考虑上面的结论:InternalTreadLocal内部使用一个数组,并且set和get均使用一个index的下标方式(不是hash的方式,再对数组长度取余),直接进行读写,并且都是static的方式,InternalTreadLocal实例是不会被JVM回收的,能够理解为Key部分不会被回收,只有Value部分可能被临时覆盖,致使老值被回收。
这里也从另一个侧面解释了为何InternalTreadLocal更快:由于InternalTreadLocal的set和get就是数组直接访问,且根本不考虑垃圾回收。ThreadLocal要清理里面的Map的垃圾数据,又没有定时线程主动触发清理(实际上也没有其余线程可用,由于每一个线程只能管本身的map结构),只能依赖set和get函数来被动地触发垃圾清理,更致使了性能在极限状况下更慢一点。