关于threadlocal
threadlocal的实现
答
ThreadLocal对象可以提供线程局部变量,每个线程Thread拥有一份自己的副本变量,多个线程互不干扰。
threadlocal的内部结构
以前的设计
每个ThreadLocal都创建一个Map,然后用线程作为Map的key,要存储的局部变量作为Map的value,这样就能达到各个线程的局部变量隔离的效果。这是最简单的设计方法
jdk8 开始
每个Thread维护一个ThreadLocalMap,这个Map的key是ThreadLocal实例本身,value才是真正要存储的值Object。
- Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。
- ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal(它的一个弱引用),value为代码中放入的值。
- 每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
- 无论是 get()、set()在某些时候,调用了 expungeStaleEntry 方法用来清除 Entry 中 Key 为 null 的 Value,但是这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄露。只有 remove() 方法中显式调用了 expungeStaleEntry 方法。
jdk8 优化后的好处
答
- 每个Map存储的Entry数量就会变少,也减少了hash冲突。因为之前的存储数量由Thread的数量决定,现在是由ThreadLocal的数量决定。在实际运用当中,往往ThreadLocal的数量要少于Thread的数量。
- 当Thread销毁之后,对应的ThreadLocalMap也会随之销毁,能减少内存的使用。
synchronized 与 threadlocal 的区别?
答
synchronized | threadlocal | |
---|---|---|
原理 | 加锁,排队访问(时间换空间) | 为每个线程复制一份变量副本(空间换时间) |
侧重点 | 多线程间资源的同步 | 多线程中数据隔离 |
ThreadLocal.set()
答
- 首先获取当前线程,并根据当前线程获取一个Map
- 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)
- 如果Map为空,则给该线程创建 Map,并设置初始值
ThreadLocal.get() 的简单理解
答
- 首先获取当前线程, 根据当前线程获取一个Map
- 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的Entry e,否则转到4
- 如果e不为null,则返回e.value,否则转到4
- Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map
总结: 先获取当前线程的 ThreadLocalMap 变量,如果存在则返回值,不存在则创建并返回初始值。
ThreadLocalMap
ThreadLocalMap 的实现
答
类似hashmap,但没有实现map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现。
threadLocal的内存泄漏问题
答
- key以弱引用的方式指向threadlocal.
- 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收,key也被回收了.
- 但是我们的value却不能回收,因为存在一条从current thread连接过来的强引用.只有当thread结束以后,强引用断开, value才会被GC回收。
- 所以当使用线程池的时候,线程结束是不会销毁的,会再次使用的就可能出现内存泄露 。
解决办法
- 避免key被自动回收,使用static修饰。 使用static final修饰threadLocal保留一个全局的threadLocal方便传递其他value(threadLocal一直被强引用)。这样就不会让gc回收作为key的threadLocal。即不会导致key为null。
- 手动执行remove()。使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。
为什么使用弱引用而不是强引用?
答
使用强引用
- 假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。
- 但是因为threadLocalMap的Entry强引用了threadLocal,造成threadLocal无法被回收。
- 在没有手动删除这个Entry以及CurrentThread依然运行的前提下,始终有强引用链 threadRef->currentThread->threadLocalMap->entry,Entry就不会被回收(Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏。
也就是说,ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的。
使用弱引用
- 同样假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。
- 由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例, 所以threadlocal就可以顺利被gc回收,此时Entry中的key=null。
- 但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 threadRef->currentThread->threadLocalMap->entry -> value ,value不会被回收, 而这块value永远不会被访问到了,导致value内存泄漏。
也就是说,ThreadLocalMap中的key使用了弱引用, 也有可能内存泄漏。
出现内存泄漏的真实原因
- 没有调用 remove方法
- thread 线程不会被回收
为什么要使用弱引用
事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value回收。
这就意味着使用完ThreadLocal,CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。
ThreadLocalMap的 Hash 算法
答
下标计算方式:
int i = key.threadLocalHashCode & (len-1);
每当创建一个ThreadLocal对象,这个ThreadLocal.nextHashCode 这个值就会增长HASH_INCREMENT( 0x61c88647 )。这个值很特殊,它是斐波那契数 也叫 黄金分割数。hash增量为 这个数字,带来的好处就是 hash 分布非常均匀。
ThreadLocalMap的 Hash 冲突以及处理
- 虽然ThreadLocalMap中使用了黄金分割数来作为hash计算因子,大大减少了Hash冲突的概率,但是仍然会存在冲突。HashMap中解决冲突的方法是在数组上构造一个链表结构,冲突的数据挂载到链表上,如果链表长度超过一定数量则会转化成红黑树。
- 而 ThreadLocalMap 中并没有链表结构,所以这里不能使用 HashMap 解决冲突的方式了。而是使用线性探测法。当发生hash冲突时,就会线性向后查找,一直找到 Entry 为 null 的槽位才会停止查找,将当前元素放入此槽位中。若整个空间都找不到空余的地址,则产生溢出。可以把Entry[] table看成一个环形数组。溢出就会执行到后面的清理空闲槽位,条件满足时就会rehash();
ThreadLocalMap.set()详解
答
- 首先还是根据key计算出索引 i,然后查找i位置上的Entry,
- 若是Entry已经存在并且key等于传入的key,那么这时候直接给这个Entry赋新的value值,
- 若是Entry存在,但是key为null,则调用replaceStaleEntry来更换这个key为空的Entry,
- 不断循环检测,直到遇到为null的。
- 如果循环结束都没有return,那么就需要新建一个Entry,并且插入,同时size增加1。
- 最后调用cleanSomeSlots,清理key为null的Entry,再判断sz 是否达到了rehash的条件((数组长度的 2/3),达到的话就会调用rehash函数执行一次全表的扫描清理。
- rehash()中会先进行一轮探测式清理,清理过期key,清理完成后如果size >= threshold - threshold / 4,就会执行真正的扩容逻辑
ThreadLocalMap过期 key 的清理流程
答
探测式清理流程(expungeStaleEntry)
遍历散列数组,从开始位置向后探测清理过期数据,将过期数据的Entry设置为null,沿途中碰到未过期的数据则将此数据rehash后重新在table数组中定位,如果定位的位置已经有了数据,则会将未过期的数据放到最靠近此位置的Entry=null的桶中,使rehash后的Entry数据距离正确的桶的位置更近一些。
启发式清理流程(cleanSomeSlots)
在添加新元素或删除另一个陈旧元素时调用。它执行对数次扫描,循环 log2n次,如果在循环中发现了 过期key,此时会进行探测式清理,并重置循环次数为 log2n次
ThreadLocalMap的扩容
答
在 ThreadLocalMap.set() 方法的最后,启发式清理未删除任何条目,且散列数组中条目的数量已经达到扩容阈值,就开始执行 rehash() 逻辑。
- 探测式清理所有过期数据,如果依然不能缩小表空间,则扩容
- size >= threshold * 3/4,时执行resize
- 扩容后的tab的大小为原理两倍,然后遍历老的散列表,重新计算hash位置,然后放到新的tab数组中,如果出现hash冲突则往后寻找最近的entry为null的槽位,遍历完成之后,oldTab中所有的entry数据都已经放入到新的tab中,并重新计算tab下次扩容的阈值。
InheritableThreadLocal
解决子线程无法共享父线程中创建的线程副本数据问题。实现原理是子线程是通过在父线程中通过调用new Thread()方法来创建子线程,Thread#init方法在Thread的构造方法中被调用。在init方法中拷贝父线程数据到子线程中。但InheritableThreadLocal仍然有缺陷,一般我们做异步化处理都是使用的线程池,而InheritableThreadLocal是在new Thread中的init()方法给赋值的,而线程池是线程复用的逻辑,所以这里会存在问题。
当然,有问题出现就会有解决问题的方案,阿里巴巴开源了一个TransmittableThreadLocal组件就可以解决这个问题