ThreadLocal

原理

在这里插入图片描述
由于ThreadLocalMap中的Entry的key持有的是ThreadLocal对象的弱引用,当这个ThreadLocal对象当且仅当被ThreadLocalMap中的Entry引用时发生了GC,会导致当前ThreadLocal对象被回收;那么 ThreadLocalMap 中保存的 key 值就变成了 null,而Entry 又被 ThreadLocalMap 对象引用,ThreadLocalMap 对象又被 Thread 对象所引用,那么当 Thread 一直不销毁的话,value 对象就会一直存在于内存中,也就导致了内存泄漏,直至 Thread 被销毁后,才会被回收。

解决办法:

我们知道出现内存泄漏的原因是失去了对ThreadLocal对象的强引用,避免内存泄漏最简单的方法就是始终保持对ThreadLocal对象的强引用,为每个线程声明一个对ThreadLocal对象的强引用显然是不合适的(太麻烦且缺乏声明的时机),所以,我们可以将ThreadLocal对象声明为一个全局常量,所有的线程均使用这一常量即可。

ThreadLocalMap
这里只提及一点,ThreadLocalMap并不适合存储大量的对象。ThreadLocalMap的Hash冲突解决办法是采用线性探测的方式,根据 key 计算 hash 值,如果出现冲突,则向后探测,当到哈希表末尾的时候再从0开始,直到找到一个合适的位置。这种算法也决定了 ThreadLocalMap 不适合存储大量数据。
ThreadLocalMap扩容原理原理类似于CopyOnWriteArrayList,ThreadLocalMap 初始大小为 16,加载因子为 2/3,当 size 大于 threshold时,就会进行扩容。扩容时,新建一个大小为原来数组长度的两倍的数组,然后遍历旧数组中的 entry 并将其插入到新的hash数组中,在扩容的时候,会把 key 为 null 的 Entry的 value 值设置为 null,以便内存回收,减少内存泄漏问题。

应用

场景1:每个线程需要一个独享的对象(通常是工具类,比如:SimpleDateFormat和Random) 我们就可以用ThreadLocal来保存对象。
场景2:每个线程需要保存全局变量,可以让不同方法直接使用,避免参数传递的麻烦(如:在拦截器中获取用户信息)

ThreadLocal 的特性也导致了应用场景比较广泛,主要的应用场景如下:
•线程间数据隔离,各线程的 ThreadLocal 互不影响
•方便同一个线程使用某一对象,避免不必要的参数传递
•全链路追踪中的 traceId 或者流程引擎中上下文的传递一般采用 ThreadLocal
•Spring 事务管理器采用了 ThreadLocal
•Spring MVC 的 RequestContextHolder 的实现使用了 ThreadLocal
•一个APP多个数据源,来回切换多个数据源进行查询数据。
•日期格式化实例多线程安全问题。

ThreadLocal什么时候会内存泄露

内存泄露问题
当把threadlocal变量置为null以后,没有任何强引用指向threadlocal实例,只有一条与key关联的弱引用路径,所以threadlocal将在下一次gc时被回收。这样一来,ThreadLocalMap中就会出现key为null的Entry,而这些key又是无法被访问到的,所以如果当前线程一直存活的话,就会存在这样一条强引用链:

CurrentThreadRef -> CurrentThread -> ThreaLocalMap -> Entry -> value

而这块value永远不会被访问到了,数据也一直存在于内存中,内存泄露的问题就产生了。

为什么要使用弱引用?
首先明确一点,内存泄露的问题和弱引用没有任何关系,使用弱引用的原因正是为了方便对象的回收和避免内存泄露,假设threadlocal的实例与key之间是强引用的关系,那么即使threadlocal自身的引用断了以后也不能对threadlocal进行回收,因为threadlocal还和key存在着强引用的关系。

如何避免内存泄露?

造成内存泄露的根本原因是因为当前线程引用着Entry对象,导致Entry对象不能被回收,从而导致value中的数据占用着内存空间。

其实要解决此问题,只需要我们使用完之后再手动调用remove方法即可。

1、【因素1:WeakReference】线程的ThreadLocalMap中的K-V的K(ThreadLocal)是以一个弱引用身份存在的,因此当没有外部强引用来引用ThreadLocal时,在下次GC时此ThreadLocal会被回收。这个时候就会出现Entry中Key已经被回收,出现一个null Key的情况,Value讲永远不会被读取到。

2、【因素2:线程一直未退出】如果当前线程的生命周期很长,一直存在,那么其内部的ThreadLocalMap对象也一直生存下来,这些null key就存在一条强引用链的关系一直存在:Thread –> ThreadLocalMap–>Entry–>Value,这条强引用链会导致Entry不会回收,Value也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。

【重点】良好的remove习惯

既然Key是弱引用,那么我们要做的事就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。

良好的remove习惯可以加速ThreadLocal Value内存的释放。

如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露/内存溢出(因为会延迟到下一次调用ThreadLocal方法,有些时候可能没有下一次了)。

为什么ThreadLocalMap的Key是弱引用类型呢?

如果 key 使用强引用:引用的ThreadLocal的对象Value被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

如果 key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值