早时总结过《ThreadLocal解析》、《FastThreadLocal解析》
最近看些资料时,又注意到这个类,不尽想再重温下,很多知识点,之前已经总结了,此篇主要有两个问题:
1、弱引用的意义
2、如何防key冲突
弱引用
ThreadLocal底层使用的ThreadLocalMap一直在进化中,早在JDK1.3时代还是使用的普通的HashMap,后来才改写成ThreadLocalMap
这个ThreadLocalMap有两个特殊之处,一是使用了线性探索法,详情见《Hashmap源码解析》;二是key使用了弱引用
1 | /** |
从注释可以看出作者为什么使用弱引用,为了处理大对象和长周期对象,在GC时可以主动回收
一直感觉这儿的弱引用设计是个鸡肋
在实际使用中,还是会出现内存泄漏,何必使用弱引用呢?
在ThreadLocal的类注释中
1 | This class provides thread-local variables. These variables differ from |
建议使用static修饰
阿里规范也有此建议:
【参考】ThreadLocal 对象使用 static 修饰,ThreadLocal 无法解决共享对象的更新问题。 说明:这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量, 也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可 以操控这个变量。
在 stackflow上点赞比较多的回答:
Because if it were an instance level field, then it would actually be “Per Thread - Per Instance”, not just a guaranteed “Per Thread.” That isn’t normally the semantic you’re looking for.
Usually it’s holding something like objects that are scoped to a User Conversation, Web Request, etc. You don’t want them also sub-scoped to the instance of the class.
One web request => one Persistence session.
Not one web request => one persistence session per object.
结合实际项目中都是使用线程池的,所以线程基本也是常驻内存的,根据建议使用static修饰,根本没有被回收的机会,虽是弱引用,但一直被强引用
所以何必呢,还让这个点成为一个常备的面试题,同是程序员,非得难为自己人呢!
上面的结论其实还是从大多数人的思维方式思考的,就是提到ThreadLocal都会与线程联系上,在线程的背景里讨论ThreadLocal
但从类注释上看,它是个变量:This class provides thread-local variables,所以还是从变量角度考虑
从变量范围讲,有类变量,局部变量,那ThreadLocal就是线程范围内的变量
根据变量的作用域,可以将变量分为全局变量,局部变量。简单的说,类里面定义的变量是全局变量,函数里面定义的变量是局部变量。
还有一种作用域是线程作用域,线程一般是跨越几个函数的。为了在几个函数之间共用一个变量,所以才出现:线程变量,这种变量在Java中就是ThreadLocal变量。
全局变量,范围很大;局部变量,范围很小。无论是大还是小,其实都是定死的。而线程变量,调用几个函数,则决定了它的作用域有多大。
ThreadLocal是跨函数的,虽然全局变量也是跨函数的,但是跨所有的函数,而且不是动态的。
ThreadLocal是跨函数的,但是跨哪些函数呢,由线程来定,更灵活
1 | public class ThreadLocalDemo |
这个示例中,count要不要被回收
我想,此时JDK的作者也是左右为难,从眼见为实的角度来说,变量不用了就应该进行回收,实现内存的自动回收,这是Java给人最大的特点。
但是,此时不能回收count,因为它与10绑定到了一块,而且只能通过count才能读写10。最后JDK作者耍了一个小聪明,用弱引用包装了count,没有干脆利索的进行内存回收,而是拖拖拉拉的进行回收,反正,最后实现了变量不用就回收的基本原则,与Java的传统思想一脉相承
到此,应该是知道为什么作者要设计为弱引用了,按建议使用ThreadLocal,修饰为static,就不是为了防内存泄漏,而只是为了达到java变量不使用就回收的基本原则,只是在线程范围时,只能曲线救国了
防碰撞
在hash数据结构中,最重要的一点就是如何更好地设计,尽量避免key的冲突
普通的hashmap使用的是拉链法,把长度设置成2的N次方,还有负载因子
ThreadLocalMap使用的线程探索法,作者使用了哪些奇技淫巧
map的长度
1 | /** |
ThreadLocalMap没有设置容量的方法,一般情况下,也不会实例化那么多ThreadLocal实例
必须是2的N次方,这个特性在HashMap中一样,就不解释了,详情见《Hashmap源码解析》
负载因子
在hashmap中,默认负载因子为了0.75,在ThreadLocal中呢?
1 | /** |
看出负载因子为2/3
但threadlocalmap还有特别的地方,就是弱引用,key可能变成null,所以在get,set都会清理key为null的Entry
1 | tab[i] = new Entry(key, value); |
再看rehash();
1 | /** |
rehash方法并不是直接就rehash了,会先清理过期元素,再判断size>= threshold-threshold/4
threshold=len * 2 / 3
threshold-threshold/4=len 2 / 3-(len 2 / 3)/4= len/2
所以最终是在清理过期元素后,元素数量超过len/2时,就会正式扩容两倍
0x61c88647
hash函数的设计是hash类数据结构的一个重点,空间与时间的平衡是关键
1 | /** |
每当创建ThreadLocal实例时这个值都会累加 0x61c88647
0x61c88647转化成十进制:2654435769为32位无符号整数的黄金分割值,而-1640531527就是32位带符号整数的黄金分割值
当容量为2的N次方,并且使用上这个魔法值后,元素经过散列算法后恰好填充满了整个容器,也就是实现了完美散列
对于这个数字由来,我也大致搜索了些资料,这已经超出作为一名码农的能力范围,只能反思数学老师讲的:不是数学用不上,而是你没有用上的能力
环形
1 | /** |
在寻找下一个位置时,虽然threadlocalmap是线性探索,但逻辑上使用环形结构,这个与防冲突关系不大,但也是个知识点,一并罗列下