欢迎访问喜蛋文章网
你的位置:首页 > 经典文章 > 文章正文

threadlocal value为什么不是弱引用

时间: 2023-04-14 15:01:20 | 来源: 喜蛋文章网 | 编辑: admin | 阅读: 107次

threadlocal value为什么不是弱引用

ThreadLocal 实现原理是什么 & 有哪些引用类型及使用场景?

对于一个 ThreadLocal 对象,通常会有两个引用指向它:

key 是弱引用,当不存在外部强引用时,会被自动回收。而 value 是强引用,引用链如下

所以只有当 Thread 被回收,value 才会被回收,否则 value 将一直存在,但是让每个线程关闭,是不现实的。在线程池中,大部分线程会伴随着系统的整个周期,那么 value 可能会造成泄漏。

解决方法,在 ThreadLocalMap 进行 set(),get(),remove() 的时候,都进行清理:

真正回收 value 的是 expungeStaleEntry() 方法,在 remove 和 set 方法中都会调用这个方法。

ThreadLocal 为了避免内存泄露,不仅使用了弱引用维护 key ,还在每个操作上检查 key 是否被回收,进而再回收value。

1、强引用

2、软引用

3、弱引用

4、虚引用

threadlocal

为每一个使用这个变量的线程提供一个变量副本,内部有一个threadlocalmap类,存储对象是entry,不存链表(开放地址法),继承自弱引用,其中key是threadlocal引用,是一个弱引用,value是一个object。

threadlocal只是相当于一个工具,负责threadlocalmap值的存取和删除,map存在于thread的属性。通过threadlocal方法的get和set等方法,实现不同线程之间各自线程变量的控制,线程隔离。对threadlocal的操作其实就是对当前线程内部threadlocalmap的操作。

get:

  1.获取当前线程,通过当前线程获取当前线程的threadlocalmap。

   2.如果存在,根据当前threadlocal作为key,通过开发地址法获取hashcode槽,对应槽如果key相等返回结果,

   3.如果不相等接着往下一定步长找下一个槽位同时遇到key为空的清除(防止内存泄漏)

    4.查询不到会进行初始化返回。过程中,如果key为空会清理key开始往后的所所有脏槽点,e=null结束

set方法(rehash扩容):

 跟get类似,如果找到对应的key的话替换,如果对应槽为空的话清除,往前接着找,同时清除过期槽,如果发生hash冲突,新建entry然后放入map,最后再判断是否清除过,如果未清除过并且长度大于等于阈值(长度的2/3),c尝试清除过期槽。然后比较清理之后长度是否大于阈值4/3,扩容两倍

remove方法:

不仅清除当前key,脏槽点key也会清除

开放地址发:根据hash计算槽位,冲突的话一定步长接着找。threadlocal用的这个的原因其实是hash计算方法散列比较均匀,效率也更高,存储数据量也不大。

其实就是一个原子类不停地去加上 0x61c88647 ,这是一个很特别的数,叫斐波那契散列(Fibonacci Hashing),斐波那契又有一个名称叫黄金分割,也就是说将这个数作为哈希值的考量将会使哈希表的分布更为均匀

threadlocal的生命周期跟着当前线程走,key为弱引用,但是value是强引用,当key为null后,如果线程执行时间比较长,value一直不释放就会内存溢出,解决方法一般尽量set完不用的话就remove掉。

get,set,remove,resize都会进行清理key为null的槽,防止泄漏的一个思路是调用这些方法。

清理过程:

1.set获取到索引下标,当key=null,调用replaceStaleEntry,会首先查找当前下标往前查找,找到最靠前的脏key,到e=null结束,记做clearSlot开始位置,往后查找,找到key相等的和replaceStaleEntry入参脏数据槽点数据互换,然后开始cleanSomeSlots清理

2.循环往前查找不到脏槽点,就当前槽点往后查找,e=null结束,找到脏槽点,以此槽点开始进行清除

3.往后往前都找不到脏槽点,不清理

整个过程找到有key相等的话交换数据

如果根据hash查找不到,新建e追加到散列地址最后的hash位置。然后cleanSomeSlots

cleanSomeSlots:

从当前槽点开始循环log2(n)次,一直往后查找是否有脏槽点,没有的话结束,有脏槽点从脏槽点开始往后清理(expungeStaleEntry返回最后一次清理位置,e=null结束),根据expungeStaleEntry返回位置为起点重新循环log2(n)次,相当于范围不断扩大

文档: https://www.jianshu.com/p/dde92ec37bd1

ThreadLocal是如何实现保存线程私有对象的

最早知道ThreadLocal是在Looper的源码里,用一个ThreadLocal保存了当前的looper对象。

当时就查了下ThreadLocal,发现是保存线程私有对象的容器,以为就是一个类似hashmap,用线程做key,存value。最近看了下并不是如此,实际上是以ThreadLocal自身为key来存储对象的。于是来学习下ThreadLocal源码是如何做到保存线程私有对象的。

ThreadLocal、ThreadLocalMap、Thread之间的关系和我们下意识中想的不太一样,不过一步步看下去之后就能明白为啥ThreadLocal能保存线程私有对象了。

这个是Thread源码,每一个Thread都有一个ThreadLocalMap对象,而ThreadLocalMap是ThreadLocal的内部类,是实际存储对象的容器。

基本关系知道了,最后再来看看ThreadLocal的方法:

那么现在基本完全清楚ThreadLocal与Thread还有ThreadLocalMap之间的关系了。
每个Thread都有一个成员变量ThreadLocalMap。这个ThreadLocalMap对象不是public,所以外部调用不了,可以看做是线程私有对象。
ThreadLocalMap存了一个table,table里保存了一些entry,每个entry对应一个key和value,而这个key就是ThreadLocal对象。
因此一个ThreadLocal只能存一个value,但是可以通过new多个ThreadLocal来保存多个线程私有对象。

在上面的源码中我们看到Entry里持有的ThreadLocal对象是弱引用持有,因此ThreadLocal不会因为线程持有而泄露,比如我们Android的主线程,正常使用过程中是不会挂掉的。
但是Enrty的value的是强引用的,因此ThreadLocal中的value还是会因为线程持有而无法回收。如果继续看源码的话,会发现在ThreadLocalMap的resize和expungeStaleEntry方法里会检查key为空的value值为空帮助GC。
因此为了避免value内存泄露,我们需要在ThreadLocal不需要的时候主动remove掉。

ThreadLocal通过自身的threadLocalHashCode来碰撞得到自己在ThreadLocalMap的table里的索引i。因此这个threadLocalHashCode就十分重要了。
这里需要保证threadLocalHashCode是唯一的,否则两个线程同时创建ThreadLocal得到相同的hashcode,那就破坏了ThreadLocalMap的线程私有特性了。
这里生成threadLocalHashCode是通过一个静态对象nextHashCode不断增加来获得的。那怎么保证多个进程并发实例化ThreadLocal对象,不会生成相同的hashcode呢?
答案是AtomicInteger,通过这个类来保证变量自增操作在多线程操作时候的原子性。
我们知道Synchronized也可以保证原子性,但相比于它,AtomicInteger类是通过非阻塞方法来实现原子性的,需要的性能开销更小。
这种非阻塞实现原子性的方法和处理器的CAS指令有关,感兴趣的小伙伴自行研究吧~

ThreadLocal 详解

ThreadLocal 是一个线程的内部存储类,对于被存储的对象,在不同的线程读取的变量是独立的。

实现原理是:对每一个线程都有一个ThreadLocalMap,ThreadLocal维护每个ThreadLocalMap中的值
ThreadLocalMap 内部是一个[]Enter, 不同的ThreadLocal都是存储在线程的同一个ThreadLocalMap中的,只是下标位置不同,
同一个ThreadLocal在不同线程的ThreadLocalMap中的下标值即索引值是相同的。

ThreadLocal 最常用的示例:

在主线程初始化ThreadLocal实例,在各个线程调用set、get,设置、获取存储在各个线程中的值

查看源码

当调用set函数时,会去获取当前线程的ThreadLocalMap对象,该对象是在Thread.java中申明,默认值为null。
当map为null时,则调用createMap,为threadLocals对象赋值,不为null,在调用ThreadLocalMap中的set函数,将值保存到数组中

当调用get方法时,获取当前线程的ThreadLocalMap对象,如果map不为null,则获取map持有的Entry对象,再返回该Entry对象持有的value值。
如果map为null或者获取的Enter对象为null,则会调用setInitialValue,而initialValue的返回值是null。
当map为null时,会调用createMap方法,实例化ThreadLocalMap

上面的set、get都会调用getMap方法,来获取当前线程的ThreadLocalMap实例

threadLocals 是在Thread.java中声明的,默认值为null,也就是说每个线程中都有这个对象,只是默认是null。

在set、get中都会对当前线程的ThreadLocalMap对象判断,当为null时,会调用createMap对ThreadLocalMap对象threadLocals赋值,

getEntry 函数就是获取key对应的节点Entry
在getEntry、set函数中可以看到value存储在[]Entry中的下标位置是由 key.threadLocalHashCode & (len-1)计算得出的。
就是ThreadLocal中的threadLocalHashCode 对[]Entry长度取模
getEntry,通过下标获取e,如果不为null而且再次校验key相等,则返回e
set时,e不为null,而且key相等,代表已存在,则替换e.value,
key不相等,代表不存在,而添加

当Entry[] 中存入的值数量已达到数组长度的3/4;
则会调用resize函数,调整Entry[]的长度,
将新数组长度*2,遍历老数组,
重新获取下标h,判断h处是否有值,无值填充,有值则重新获取h,再填充

ThreadLocalMap 的内部类

Entry继承WeakReference,说明ThreadLocal内部存储的类型都是采取弱引用累心存储,当GC时,则会被回收。
这样保证当线程执行完时,当前线程中存储在ThreadLocalMap中的对象会被回收,不会在此处出现内存泄漏

value是调用ThreadLocal保存的值,
文章标题: threadlocal value为什么不是弱引用
文章地址: http://www.xdqxjxc.cn/jingdianwenzhang/168355.html
文章标签:引用  threadlocal
Top