ThreadLocal和相关的内容
一、 略过但是需要掌握的内容
1、 ThreadLocal的作用与使用
2、 ThreadLocalMap的结构与和Thread的关系
3、 ThreadLocalMap中的Entry为什么Key是WeakReference
4、 ThreadLocal是否会内存泄露
上面这些内容可以自行百度,先针对第四个问题做一些展开
二、 ThreadLocal的内存泄露条件以及不完善内存泄露防范
ThreadLocal的内存泄露的三个条件:
1、 使用线程池,线程结束Entry[]的强引用会被全部GC,所以不会内存泄露
2、 Key被回收,Value作为强引用一直存在
3、 重点:内存防范没有处理到Key为NULL的情况
2.1 ThreadLocalMap的冲突机制
总所周知HashMap无论使用什么散列算法,都是存在Hash冲突的,常见的Hash冲突的解决方法是一个数据+多个相同HashCode的链表的方式,当HashCode冲突后,往链表添加数据,查找的时候遍历链表。具体可以百度或查看Java中的HashMap的实现。
那么ThreadLocalMap是如何解决冲突的呢? SET方法,如图一所示
图一 ThreadLocalMap的Set方法
从倒数第四行代码来看,他是添加到从散列值到第一个为NULL的数组中。
而在查找中,我们也能找到对应的逻辑,当key不一致时候,从散列函数获取位置往后查找,如图二所示
在这个函数中,我们还能看到以下两个点:
1、 清除Key为NULL值,为什么笔者说不完善呢,下个章节分析
2、 扩容,大致的扩容方案是从16开始,大于等于2/3原长度时候进行扩容,扩容长度原长度两倍。
同样这里也有一个思索,为什么不用相对标准的HashMap结构呢,这种结构会引起连锁的冲突,目前笔者没答案,欢迎讨论。
图二 ThreadLocalMap的Get方法
2.2 ThreadLocalMap的不完备防止内存泄露机制
在上面的Get和Set代码中,我们看到了有处理Key为null的情况,网上有一种说法是Get和Set的时候会清理Key值为null的元素。实际并非这样的。
看get的代码,当通过Hash进行查找到数组元素的时候,同时Key的值等于查找的Key的值的时候,则不会进行其他操作,也就是说在没有冲突的情况下,是不会进行清理的。那么如果冲突了,是否是对所有key为null值进行清理呢?答案也不是的,如果找到key值相同时则不会进行清理,同样清理的初始值也是hash之后的i开始。所以get的时候是个不完备的清理。
再看set的代码,同样是从Hash之后的i进行查找,如果找到相同的key时则进行设置,如果没找到则清理后续所有的key为null的value,同样也是非完备的清理。
三、 可能存在的改进点
由于冲突的存在和不完备的清理工作,因此无论插入、删除、查找都不是O(1)的时间复杂度。同样由于不完备的清理工作,在实际使用过程中尽可能地通过remove进行完备清理来避免内存的泄露。
那么是否有改进的空间吗?答案是有的。
3.1 FastThreadLocal和InternalThreadLocal
在Netty框架中,对ThreadLocal进行了改造,不再采用HashMap的方式进行保存,而采用顺序数组的方式;在Dubbo中,参考了FastThreadLocal,下面我们对InternalThreadLocal进行分析。
先看InternalThreadLocalMap的结构,如图三:
图三 InternalThreadLocalMap的结构
其中slowThreadLocalMap为了兼容普通的线程,而结构不再是Entry[],而是Object[],那么是如何进行查找的呢,我们同样看Get和Set方法,如图四所示:
图四 InternalThreadLocal的Get/Set
这里有个至关重要的变量index,那么这个index是如何维护的呢?我们再看源码,如图五所示
图五 Index的维护
Index不再是像ThreadLocal一样去加一个魔数,而是加1。同时每个InternalThreadLocal自己保存着自己的Index。这样做至少有以下两个好处:
1、 省去了Hash取余操作(这个取余在JDK中已经为了提高效率采用位操作替代%,当然付出的代价就是必须长度是16的倍数)
2、 避免了冲突和清理工作,使时间复杂度为O(1)
3.2 一个疑问点与改进方式
然而,笔者有一个地方觉得比较奇怪,就是图三中的NEXT_INDEX为什么是static类型,这样造成的直接后果就是假设有1000个InternalThreadLocal变量,那么几乎所有线程都可能要扩容到1024,即便这个线程只有一个变量,如果线程是销毁的,那很快就会循环出现一次异常?为此还付出了removeall的代价,因为扩容可能造成大量元素是空元素,所以遍历效率太低。
笔者尝试把NEXT_INDEX进行修改,只有当前线程的变量超过32的时候才进行扩容,git地址为:https://taou.cn/jQfVd
至此,我们完成了ThreadLocal的分析和更高效率的扩展。那么在线程池中,又如何使用呢?
四、 线程池的使用InternalThread
那么在线程池又如何使用到InternalThread呢,我们知道线程池使用ThreadPoolExecutor,而ThreadPoolExecutor可以使用方法setThreadFactory(ThreadFactory threadFactory)进行设置线程工厂类,我们看dubbo的线程工厂的定义,图六
图六 Dubbo中的线程工厂
其中,Named并非必须。
那么在SpringBoot中又如何使用线程池呢?SpringBoot线程池类ThreadPoolTaskExecutor,在这个类中同样提供了setThreadFactory,因此我们同样在创建bean的使用设置自定义的线程工厂即可。