ThreadLocal
Thread有成员变量ThreadLocalMap对象(threadlocals)
ThreadLocal有内部类ThreadLocalMap用于存放线程数据
ThreadLocalMap底层通过Entry数组保存数据
所以其实是每个线程都有一份自己的ThreadLocalMap用于存放所有的entry,独立管理,每个threadlocals都是直接放在线程本身内部当做成员变量的,而每个new出来的threadlocal则是与值封装成Entry,类似key的功能。
Entry继承了弱引用的ThreadLocal,设置时会将ThreadLocal设置进去,当做key
1 tab[i] = new Entry(key, value);

为什么是弱引用?
一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
所以避免了key的内存泄露,但value还是需要手动使用set或remove回收
一个线程可以有多份数据存储,通过不同的threadlocal对象来获取

1 | /** |
底层通过AtomicInteger维护一个静态成员变量实现ThreadLocal的哈希值生成
AutomicInteger可以保证多线程情况下创建threadLocal得到的hash值必定不同,避免了equals不同而hash值相同的情况。
每当创建一个
ThreadLocal对象,这个ThreadLocal.nextHashCode这个值就会增长0x61c88647。这个值很特殊,它是斐波那契数 也叫 黄金分割数。
hash增量为 这个数字,带来的好处就是hash分布非常均匀。
ThreadLocalMap使用闭散列:(开放地址法或者也叫线性探测法)解决哈希冲突,线性探测法的地址增量di = 1, 2, … 其中,i为探测次数。该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。
按照上面的描述,可以把table看成一个环形数组。
对比FastThreadLocal
其实就是FastThreadLocal使用index直接记录元素下标,避免了hash计算,提高了效率。
疑惑
- 为什么NextIndex是static的
补充与思考
ThreadLocal为了防止内存泄露所做的
ThreadLocal通过弱引用使得reference,也就是ThreadLocal这个key可以被回收(在只有这个弱引用关系时)
如果set的是一个全新为null的slot,而非被回收了弱引用的,也非正常使用的entry,那么set时会顺带进行一次启发式清理,这个清理是从当前下标开始清理log2(n)次(n是当前size,也就是元素数量)
这里只举例说明了slot为null的情况,其他情况比如key为null也是会尝试进行清理的,另外get在哈希下标位置没找到的时候也会尝试进行清理
值得注意的
这里的清理是按次数来算的,每次清理都会以遇到空slot为止,并且会对清理过程中的有效entry重新hash
这里修改了Knuth高德纳的著作TAOCP(《计算机程序设计艺术》)的6.4章节(散列) 中的R算法
另外值得注意的是ThreadLocal如果出现线程复用的话可能导致脏数据问题。如果在实现的线程 run()方法体中不显示地调用 remove 清理与线程相关的 ThreadLocal 对象,并且复用时不调用 set 设置初始值,就可能 get () 到重用的线程信息。
巧妙之处
弱引用对ThreadLocal的设计上造成了一定的问题,主要体现在key被回收了而value依然存在,entry不为null。
为什么不能value也设置成弱引用的原因是因为value只被entry引用,会导致误回收
但如果结合ThreadLocal解决哈希冲突的策略(开放定址法)来看这一设计就变得比较合理了。
ThreadLocal使用开放定址法解决哈希冲突,如果当前slot已经在使用,那么需要增加下标值继续向后探测,而正是因为value并没有被回收,所以开放定址法能作为一个有效的策略。
如果整个entry都能直接被回收
考虑这种情况
以下entry都是同一哈希值,通过开放定址法集聚在一起
entry0 entry1 entry2 entry3 entry4
直接将部分entry回收
null null entry2 entry3 entry4
那么就无法通过哈希函数有效查找到后面三个entry了
所以实际上value的保留相当于打上了一个标记,为之后的查找和重哈希提供了遍历。
关于解决哈希冲突方案的选择
HashMap使用拉链法和红黑树,redis使用拉链法,而ThreadLocal使用开放定址法
除了网上都在说的红黑树更复杂,拉链法又比开放定址法复杂,以及指针带来的空间开销外
个人感觉一个重要的原因是哈希函数的限制
对于HashMap,无法确定用户会给出一个怎样的hash函数,所以需要尽可能考虑到所有的情况,需要有兜底的策略。
而对于redis来说,数据结构的数量和种类是固定的,哈希函数都经过精心设计,所以通过拉链法就够用了。
而对于ThreadLocal来说,哈希函数直接通过黄金分割数配合长度为2的幂的数组来使用,极大降低了哈希冲突的可能性,所以可以直接使用开放定址法即可。