jdk的threadLocal的改进
Jdk以前的实现方法是threadLocal中一个hashmap,key为每个线程,value为值;这样会带来几个弊端,一是hashmap可能会过大;二是若当前线程被结束后,key依然指向被销毁的线程并且是强引用,导致线程没有及时被清理,造成资源浪费;
但后来jdk升级了threadLocal,没采用这种实现方式;而是每个thread中有一个ThreadLocal.ThreadLocalMap类型的属性,ThreadLocalMap类是ThreadLocal类的静态内部类,ThreadLocalMap实例的key为当前ThreadLocal类型,value为ThreadLocal的子类重写initialValue方法的返回值,并且thread引用了threadLocalMap这样就使每个线程都保留自己的数据并且使threadLocalMap的生命周期和thread的生命周期绑定了;二是ThreadLocalMap中都有一个entry,且entry是通过弱引用关联key的,这样当其他强引用指向key的路径被断开后,key就可以直接被gc;
ThreadLocalMap如何用弱引用关联key
① A a = new A(),此时a属于强引用;
② WeakReference weak = new WeakReference(a),此时weak属于弱引用;
③ B b = a,设置b指向A实例,此时b属于强引用;
只有①与②组合使用时,当设置a=null,下次发生gc时,A实例就被回收了;
只有①与③组合使用时,当设置a=null,下次发生gc时,A实例不会被回收;
而在threadLocalMap中,key就是a,Entry的父类是WeakReference,就是①与②的组合,此时调用weak.get方法返回null,此时可以区分哪些entry是过期的,哪些是非过期的,这样设计有利有弊;
ThreadLocal原理
TheadLocal#get 方法
1)先获取当前线程的ThreadLocal.ThreadLocalMap类型的threadLocals属性(获取每个线程独有的map);
Thread类有一个ThreadLocalMap类型的属性threadLocals,默认为null;因为Thread.threadLocals字段是延迟初始化的,只有线程第一次存储 threadLocal-value 时 才会创建 ThreadLocalMap对象。
2)若threadLocals属性不为null,则调用ThreadLocalMap#getEntry(this)方法;
threadLocals属性不为null,说明线程已拥有自己的ThreadLocalMap对象了;调用map.getEntry() 方法 获取threadLocalMap 中该threadLocal关联的 entry;若不为null,则说明当前线程初始化过与当前threadLocal对象关联的线程局部变量;
2.1)ThreadLocalMap#getEntry方法(此时的入参为this,表示当前ThreadLocal实例);
2.1.1)进行路由,当前ThreadLocal实例的hash和数组长度减1的结果取与,得到目标桶位的索引;
2.1.2)若目标桶位不为null且目标桶位的key与查询的threadLocal相同,则返回当前entry即可;
2.1.3)目标桶位为null或者key不同,则继续往后查询;执行getEntryAfterMiss方法;因为存储时,发生哈希冲突后,threadLocalMap中并不会形成链表,而是线性的向后找一个可以使用的桶位,放进去;所以不形成链表的弊端就是当查询时,按照路由值发现桶位的不存在或者key不在该桶位,所以需要继续往后找;
2.1.3.1)getEntryAfterMiss方法中先判断若entry为null则直接返回null,若不为null则查找下一个桶位,直到桶位为null则退出循环,返回null,表示整个数组中都没有需要的ThreadLocal实例,value;
2.1.3.1.1)当当前桶位的key与需要查询的key相等,则找到了,返回当前entry;
2.1.3.1.2)若当前桶位的key为null(此时会触发探测式清理),说明当前桶位中的entry#key 关联的
ThreadLocal对象已经被GC回收了,因为key是弱引用,所以key = e.get() == null;此时会做一次探测式过期数据回收,执行expungeStaleEntry(i)方法;
a)由于当前桶位的key为null,则设置当前桶位的value为null,桶位entry为null,size–(数组长度);
b)向后循环遍历数组,直到桶位为null,则退出循环;若桶位的key为null,则threadLocal已被gc回收,此时执行和a)一样的逻辑,并且继续查看下一个桶位;
c)若key不为null且当前桶位的索引i和重新计算当前桶位key的路由值h(这个值恒定不变,且为正确值)不同,说明当前entry 存储时有可能碰到hash冲突了,往后偏移存储了,这个时候应该去优化位置,让这个位置更靠近正确位置,这样的话,查询的时候 效率才会更高!此时设置当前桶位为null,以正确位置h 开始,向后查找第一个可以存放entry的位置h1,可以存放的条件是entry为null,找到后,令tab[h1]=e;
expungeStaleEntry方法会沿着开始位置向后探测清理过期数据,沿途中碰到未过期数据会重新确定位置,即将此数据重新计算key.hash&(table.length-1),以此为起点找到entry为null的位置,再放进去,这样理论上更接近正确位置key.hash&(table.length-1);
expungeStaleEntry方法相当于检查超市货架上的商品,货架原来有4层,从上往下依次为1到4,刚进货回来时牛奶一直放在第2层,但是因为某些原因最后将牛奶放到了第4层,某天服务员发现第1层中出现了过期商品需要被移除,这个时候,就索性从第1层开始往下找,把没过期的牛奶则放到之前一直放的位置,如果被占据了,则放到最靠近之前一直放的位置,过期的商品则移除掉;
2.1.3.1.3)key既不相等,也不为null,则更新下标,查看下一个桶位entry;
3)若threadLocals属性为null或ThreadLocalMap#getEntry方法返回null,则调用setInitialValue方法;
当前线程对应的threadLocalMap是空或者,当前线程与当前threadLocal对象没有生成过相关联的线程局部变量;setInitialValue方法初始化当前线程与当前threadLocal对象 相关联的value且当前线程如果没有threadLocalMap的话,还会初始化创建map。
ThreadLocal的四种应用场景
1)一个线程中可以多次用同一个ThreadLocal实例,此时执行set方法时,如果不是第一次插入,由于ThreadLocalMap中的插入的key都是同一个key,其key.hash&(array.length-1)相等,所以会发生hash冲突,此时直接替换旧的value;如果是第一次插入,则在取&后的桶位,新建一个entry,存放key和value;若该桶位存在entry,但key为null,则说明是过期数据,需要被清理掉,即置value为null;
2)一个线程中也可以新建多个ThreadLocal实例使用,此时执行set方法时,如果该ThreadLocal实例不是第一次插入,则ThreadLocalMap中的插入的该ThreadLocal实例的key是同一个key,其key.hash&(array.length-1)相等,则发生了hash冲突,此时直接替换掉旧的value,若该ThreadLocal实例是第一次插入,则该ThreadLocal实例与其他实例的key的key.hash&(array.length-1)可能相等,但key绝对不同(因为是不同ThreadLocal实例),则会继续遍历下一个桶位,直到找到桶位的entry为null,则新建一个entry,存放value;遍历桶位有两个目的,一是找到相同的key的元素进行替换,二是若没有相同的key,则看key是否为null,若为null则进行过期元素清理;若既不相等,也不为null,则继续遍历下一个桶位,直到桶位为null;注意桶位为null和key为null是不同的意思,桶位为null表示没有entry,是第一次插入,而key为null,表示entry中key 所关联的ThreadLocal实例已经被回收了,此时需要置value也为null,避免内存泄漏;
3)多个线程可以共用一个ThreadLocal实例,注意每个线程始终都有自己的ThreadLocalMap,但这种情况下,ThreadLocal是共用的;此时在每个线程中分别执行同一个实例的set方法时,是互不影响的,各自操作各自的ThreadLocalMap,由于是同一个key,所以每个ThreadLocalMap上桶位的占用情况是一样的;
4)多个线程各自使用自己的ThreadLocal实例,这种情况就是属于完全独立了,线程之间ThreadLocalMap的桶位占用情况也不一样了;
FastThreadLocal的四种应用场景
既然ThreadLocal有四种应用场景,那么FastThreadLocal也同样有四种相同的应用场景;只是存储的数据结构不同,这里要注意index的变化,哪些情况下自增,哪些情况下不自增,以及是否是在FastThreadLocalThread线程中使用;
首先需要明确index变量的性质,是FastThreadLocal中被final修饰的实例变量,在FastThreadLocal的构造方法中调用InternalThreadLocalMap
的nextIndex.getAndIncrement()自增1,所以每new一个FastThreadLocal实例,会执行一次nextIndex.getAndIncrement(),index就会加1,并且是连续的;
接着需要明确nextIndex变量的性质,是InternalThreadLocalMap父类的static final变量,即所有实例共享;
1)一个FastThreadLocalThread线程中多次用同一个FastThreadLocal实例,此时一个线程也只对应一个InternalThreadLocalMap,由于index是FastThreadLocal中被final修饰的实例变量,所以对于每个FastThreadLocal实例,index是固定值,此时只能替换数组固定位置的value,这与利用hashmap如出一辙;
2)一个FastThreadLocalThread线程中也可以新建多个FastThreadLocal实例使用,此时每建一个FastThreadLocal实例,index++,于是每个FastThreadLocal实例在数组上占一个桶位,由于get是实例方法,所以每个FastThreadLocal实例执行get方法时,都到自己的index上去替换旧值;
3)多个FastThreadLocalThread线程可以共用一个FastThreadLocal实例,多个线程意味着多个InternalThreadLocalMap实例,但此时index依然是固定值,因为index是FastThreadLocal的实例变量,只会随着FastThreadLocal实例的增多而变化,变化逻辑取决于执行的方法逻辑,而该方法逻辑存在于FastThreadLocal的构造方法中,即自增加1;所以最终在每个数组中,同样的位置进行各自操作,相互独立,互不影响;
4)多个FastThreadLocalThread线程各自使用自己的FastThreadLocal实例,这种情况就是属于完全独立了,线程之间InternalThreadLocalMap的数组占用情况也不一样了;a线程的InternalThreadLocalMap中数组的index为1的桶位会被使用,b线程的InternalThreadLocalMap中数组的index为2的桶位会被使用,
c线程的InternalThreadLocalMap中数组的index为3的桶位会被使用;
最后空闲的桶位被UNSET变量填充,可以避免缓冲行的伪共享;
FastThreadLocal在普通线程运行会怎样
FastThreadLocal在普通线程和FastThreadLocalThread线程中运行是有区别的,在普通线程中执行FastThreadLocal#get方法,实际上最终会形成这样一种情况:会新建一个InternalThreadLocalMap实例,将其作为UnpaddedInternalThreadLocalMap中ThreadLocal类型的变量slowThreadLocalMap
中threadLocalMap的value,这是一个被final修饰的类变量,全局唯一且不变,再将自定义的FastThreadLocal实例中重写的initialValue方法的返回值作为InternalThreadLocalMap实例的value;所以此时在普通线程中执行get时,实际上会先找到ThreadLocal中threadLocalMap的value,即InternalThreadLocalMap实例,再根据index找到value,即自定义的FastThreadLocal实例中重写的initialValue方法的返回值;
如上图所示,测试了在普通线程中set,在FastThreadLocalThread中get,以及在FastThreadLocalThread线程中set,在普通线程中get的场景;
其实当在普通线程中get时,会使用公用的ThreadLocal变量,另外也会new一个InternalThreadLocalMap实例,如果没有set,则InternalThreadLocalMap中数组的value源自于new FastThreadLocal时的重写initialValue方法的返回值,如果在普通线程中有执行set(xx),则优先使用该set中的值xx;
当在普通线程中set,然后再在FastThreadLocalThread线程中get时,是拿不到的,此时在该线程中会new一个新的InternalThreadLocalMap实例,重新将new FastThreadLocal时重写的initialValue的返回值作为该实例的value;如果在本线程中有set,则优先使用set的值;
最后FastThreadLocalThread线程中的set的值,在普通线程中也无法get到,因为普通线程此时get时,只会先找到公用的ThreadLocal中的ThreadLocalMap中的value,即InternalThreadLocalMap实例(该实例和FastThreadLocalThread线程中的是不同的两个),再从该实例中找到之前set进去的值;
所以这也是在普通线程中使用FastThreadLocal效率低的原因,最后从上边测试和分析可以看出普通线程和FastThreadLocalThread线程之间也是相互隔离的;
在new ThreadLocal或者new FastThreadLocal时,传入的泛型类型和initialValue方法返回的类型必须保持一致;
其实在普通线程中调用FastThreadLocal#get方法,本质上是有以下5步:
1)先建立好普通线程的那一套ThreadLocalMap;
2)接着创建了一个InternalThreadLocalMap实例,最后将该实例作为ThreadLocalMap中的value;
3)将FastThreadLocal实现类中重写的initialValue方法的返回值,放到InternalThreadLocalMap中,并返回该值;
在普通线程中调用FastThreadLocal#set方法,会覆盖上述第3)步中的值;
在FastThreadLocalThread线程中调用FastThreadLocal#get方法,此时跟普通线程无关,会在当前ftlt线程中新建一个InternalThreadLocalMap实例,并且将FastThreadLocal实现类中的重写的initialValue方法的返回值,放到InternalThreadLocalMap中,并返回该值;
所以若普通线程和ftlt线程使用的是同一个FastThreadLocal的实现类,则必然使用的是同一个initialValue方法的返回值,此时虽然普通线程和ftlt线程中各有一个internalThreadLocalMap,但其中的value一样,但普通线程中set,只能影响到普通线程get;ftlt线程中set,也只能在ftlt线程中get时生效;
哪些框架中对ThreadLocal进行了优化
其实除了Netty中通过自定义FastThreadLocal对ThreadLocal进行了优化,在其他的框架中也有类似的优化,比如Dubbo中就InternalThreadLocal,根据源码中的注释,也是参考了FastThreadLocal的设计,基本上差不多。
另外jdk的InheritableThreadLocal可以完成父线程创建子线程的时候将InheritableThreadLocal中的值传递给子线程;
而阿里的TransmittableThreadLocal则在InheritableThreadLocal的基础上,解决了线程池中线程之间的值传递问题;因为线程池中的线程不是新建的,而是复用的,而InheritableThreadLocal只适用于新建线程继承父线程的值,不适用于复用情况;
具体可参考:
https://mp.weixin.qq.com/s/NxBLhGo6MzClGpQP9WXctA?fileGuid=Ty8hqrvhtDydKt8V
netty中的应用
jdk中有个ThreadLocal,而netty中的FastThreadLocal是它的升级版;
在netty的PooledByteBufAllocator#newDirectBuffer方法中的第一行调用了threadCache.get方法,即应用了FastThreadLocal,实现了每个线程都有独自的一个PoolThreadCache实例;
另外在Recycler类中有个FastThreadLocal<Stack>类型的实例变量,在Recycler#get方法中会调用threadLocal#get方法返回Stack实例;
FastThreadLocal的优势
FastThreadLocal操作元素的时候,使用常量下标在数组中进行定位元素来替代ThreadLocal通过哈希和哈希表,这个改动特别在频繁使用的时候,效果更加显著!
想要利用上面的特征,线程必须是FastThreadLocalThread或者其子类,默认DefaultThreadFactory都是使用FastThreadLocalThread的;
只有在FastThreadLocalThread或者子类的线程使用FastThreadLocal才会更快,因为FastThreadLocalThread 定义了属性threadLocalMap类型是InternalThreadLocalMap。如果普通线程会借助ThreadLocal。
默认的数组大小为32,且使用UNSET对象填充数组,如果下标处数据为UNSET,则表示没有数据;
https://www.jianshu.com/p/626b2be672c1
FastThreadLocal为啥要在FastThreadLocalThread线程中用才能体现优势
红色框中内容第一次执行的返回值一定为null,因为此处的ThreadLocal并没有重写initialValue方法,所以下边的if判断中的内容相当于重写ThreadLocal中的initialValue方法,即把新建的InternalThreadLocalMap实例作为普通线程的ThreadLocalMap的value;所以要找到该value后,才能再根据index在InternalThreadLocalMap中找到存储的值,所以如果FastThreadLocal不是在FastThreadLocalThread线程中执行时,而是在普通线程中执行时,先要通过hash在ThreadLocalMap中定位,再在InternalThreadLocalMap中定位,效率比单纯用ThreadLocal还低;
index的理解
在InternalThreadLocalMap的父类中定义了两个static变量,即InternalThreadLocalMap类的实例共享着两个变量,一个是当FastThreadLocal遇到普通线程时,其中使用的ThreadLocal;另一个是当FastThreadLocal遇到FastThreadLocalThread线程时,其中使用的InternalThreadLocalMap中的索引index;所以有多个FastThreadLocalThread线程a,b,c,则其数据结构中的index值依次为1,2,3;并且由于nextIndex和slowThreadLocalMap都是static final变量,所以只会随着UnpaddedInternalThreadLocalMap类的加载而只执行一次,新建再多的UnpaddedInternalThreadLocalMap实例,都不会再执行这两行了;variablesToRemoveIndex也是如此;
index的赋值在以上方法中实现,每次递增1,该方法有两处调用,一个是在FastThreadLocal的类变量variablesToRemoveIndex
初始化时,且该变量为final类型,即所有的FastThreadLocal实例中该变量都一样,为0,另一个FastThreadLocal的构造方法中初始化index;类变量的初始化优先于构造方法执行,所以
fastThreadLocal优势
1)底层实现效率更高
底层没有使用Entry类型的键值对数组,去掉了hash寻址,探测清理过期数据等操作,而是直接使用单值数组,使用常量作为下标,每个FastThreadLocal实例有一个固定下标值, 当一个FastThreadLocalThread线程中使用多个ftl实例时,此时才会在一个数组中,每个ftl实例占据一个固定下标位置,而所有的ftl实例统一存储在数组下标为0的位置统一管理;
一个Thread中使用多个ThreadLocal实例时,才会是多个值往一个threadLocalMap中插入,这种情况并不是很多,所以此时也不会发生很多探测清理等过程,仅仅是hash寻址,过期了,再次hash寻址,所以threadLocal的时间复杂度基本上为o(1),和数组一样;个人觉得底层实现上,jdk比netty不会差很远;当一个线程中使用很多ThreadLocal实例,一方面hash冲突会导致新插入的tl实例存到了其他空闲桶位,另一方面key的过期,导致程序进行不断的判断,清理,移动key操作,这些才是底层耗费时间的根本原因,并不是单纯的一句话一个用了类似hashmap的结构,一个用了数组;
2)利用字节填充来解决伪共享问题
关于字节填充解决缓存伪共享问题的具体原理与细节,参考文章
https://blog.csdn.net/qq_27680317/article/details/78486220
3)更高效的解决内存泄漏问题
https://blog.csdn.net/abc123lzf/article/details/83591922
当threadLocal失去强引用,被gc回收后,threadLocalMap中的entry的get(key)方法返回null,所以依据此来判断threadLocal是否被回收,此时只需要设置key.value为null,设置key对应的entry为null,即可防止内存泄漏;
那fastThreadLocal是如何做到的呢?
netty的线程池创建的是FastThreadLocalRunnable类型的线程,我们发现其run方法在finally块中统一执行了FastThreadLocal.removeAll()方法,这样在每次线程执行结束的时候都会清理掉ftl对象,不会造成内存溢出的问题。因为ftl也可以用于普通线程,所以在普通线程中使用的时候我们记得一定要执行removeAll方法,否则会造成内存溢出问题。
具体细节参见https://blog.csdn.net/u010386139/article/details/107501734删除数据部分;
个人认为在threadLocalMap中,threadLocal和value是以键值对的方式管理的,即存储在Entry类的数组中,而internalThreadLocalMap是将fastThreadLocal和value分开管理的,只用了一个初始长度为32的Object类型的数组,每个fastThreadLocal实例固定分配一个桶位存储value,但索引为0的桶位始终指向的是一个set集合,该集合用于存储fastThreadLocal实例;
另外在执行threadLocalMap的get,set,remove方法时,都会对entry.get(key)为null的entry进行过期清理工作,并将后续节点重新移动到更靠经正确位置的桶位;但internalThreadLocalMap执行remove方法时,是直接将fastThreadLocalThread中的InternalThreadLocalMap类型的属性指向null;
在执行FastThreadLocal#remove方法时,会依次执行InternalThreadLocalMap#removeIndexedVariable方法和FastThreadLocal#removeFromVariablesToRemove方法;前者是将当前FastThreadLocal实例在internalThreadLocalMap上分配的固定桶位设置为UNSET,后者是将当前FastThreadLocal实例从set集合中删除;
fastThreadLocal优点:
1)ftl使用了单纯的数组操作来替代了tl的hash表操作,所以在高并发的情况下,ftl操作速度更快。
2)set操作:ftl直接根据index进行数组set,而tl需要先根据tl的hashcode计算数组下标(而ftl是直接获取),然后再根据线性探测法进行set操作,其间如果发生hash冲突且有无效的Entry时,还要进行Entry的清理和整理操作。最后不管是否冲突,都要进行一次log级别的Entry回收操作,所以慢了。
3)get操作:ftl直接根据index进行获取,而tl需要先根据tl的hashcode计算数组下标,然后再根据线性探测法进行get操作,如果不能根据直接索引获取到value的话并且在向后循环遍历的过程中发现了无效的Entry,则会进行无效Entry的清理和整理操作。
4)remove操作:ftl直接根据index从数组中删除当前的ftl的value,然后从Set集合中删除当前的ftl,之后还可以进行删除回调操作(功能增强);而tl需要先根据tl的hashcode计算数组下标,然后再根据线性探测法进行remove操作,最后还需要进行无效Entry的整理和清理操作。
5)tl由于使用线性探测法,需要在get、set以及remove时做一些资源清理和整理操作,所以代码看上去不如ftl清晰明了。
缺点:
ftl相较于tl不好的地方就是内存占用大,不会重复利用已经被删除(用UNSET占位)的数组位置,只会一味增大,是典型的“空间换时间”的操作。