ThreadLocal

引言
ThreadLocal的官方API解释为:

“该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。”

大概的意思有两点:

  • ThreadLocal提供了一种访问某个变量的特殊方式:访问到的变量属于当前线程,即保证每个线程的变量不一样,而同一个线程在任何地方拿到的变量都是一致的,这就是所谓的线程隔离。
  • 如果要使用ThreadLocal,通常定义为private static类型,在我看来最好是定义为private static final类型。

应用场景

ThreadLocal通常用来共享数据,当你想在多个方法中使用某个变量,这个变量是当前线程的状态,其它线程不依赖这个变量,你第一时间想到的就是把变量定义在方法内部,然后再方法之间传递参数来使用,这个方法能解决问题,但是有个烦人的地方就是,每个方法都需要声明形参,多处声明,多处调用。影响代码的美观和维护。有没有一种方法能将变量像private static形式来访问呢?这样在类的任何一处地方就都能使用。这个时候ThreadLocal大显身手了。

synchronized是用时间换空间、ThreadLocal是用空间换时间,为什么这么说?
因为synchronized操作数据,只需要在主存存一个变量即可,就阻塞等共享变量,而ThreadLocal是每个线程都创建一块小的堆工作内存。显然,印证了上面的说法。

总结:
JVM利用设置ThreadLocalMap的Key为弱引用,来避免内存泄露。
JVM利用调用remove、get、set方法的时候,回收弱引用。
当ThreadLocal存储很多Key为null的Entry的时候,而不再去调用remove、get、set方法,那么将导致内存泄漏。
当使用static ThreadLocal的时候,延长ThreadLocal的生命周期,那也可能导致内存泄漏。因为,static变量在类未加载的时候,它就已经加载,当线程结束的时候,static变量不一定会回收。那么,比起普通成员变量使用的时候才加载,static的生命周期加长将更容易导致内存泄漏危机。

在这里插入图片描述
说明一下我自己的想法,为什么会出现内存泄漏,就是存在无用但是可达的数据,这些无用的数据无法被GC通过可达性分析给清除调。因为entry的key是弱引用,所以会每次GC会导致Key为null,而value却有一条长长的链路指向自己,导致自己可以不被GC回收,但是这个值已经没用了,如果线程迟迟不结束的话,则导致内存泄漏。
再说一下为啥要把key设置为弱引用,如果key为强引用,而ThreadLocal常常又是静态的,导致ThreadLocal会一直关联到entry,entry因为有了这个ThreadLocal这个强引用,会导致哪怕线程执行完了,entry也不能被释放,所以把ThreadLocal这个key置为弱引用,每次GC就会回收key了。

ThreadLocal的实现是这样的:每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal实例本身,value 是真正需要存储的 Object。也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。值得注意的是图中的虚线,表示 ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。

但是这些被动的预防措施并不能保证不会内存泄漏:

使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏(参考ThreadLocal 内存泄露的实例分析)。

分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。

ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

补充:

一个线程对应一个ThreadLocalMap ,可以存储多个ThreadLocal对象。

ThreadLocal对象作为key、独享数据作为value

一个线程对应一个ThreadLocalMap,get()就是当前线程获取自己的ThreadLocalMap。

线程根据使用那一小块的threadlocal,根据ThreadLocal对象作为key,去获取存储于ThreadLocalMap中的值。

怎么避免内存泄漏呢?

每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

总结

(强制)在代码逻辑中使用完ThreadLocal,都要调用remove方法,及时清理。
目前我们使用多线程都是通过线程池管理的,对于核心线程数之内的线程都是长期驻留池内的。显式调用remove,一方面是防止内存泄漏,最为重要的是,不及时清除有可能导致严重的业务逻辑问题,产生线上故障(使用了上次未清除的值)

最佳实践:在ThreadLocal使用前后都调用remove清理,同时对异常情况也要在finally中清理。

(非规范)对ThreadLocal是否使用全局static修饰的讨论。
在某些代码规范中遇到过这样一条要求:“尽量不要使用全局的ThreadLocal”。关于这点有两种解读。最初我的解读是,因为静态变量的生命周期和类的生命周期是一致的,而类的卸载时机可以说比较苛刻,这会导致静态ThreadLocal无法被垃圾回收,容易出现内存泄漏。另一个解读,我咨询了编写该规范的对方解释是,如果流程中改变了变量值,下次复用该流程可能导致获取到非预期的值。

但实际上,这两个解读都是不必要的,首先,静态ThreadLocal资源回收的问题,即使ThreadLocal本身无法回收,但线程中的Entry是可以通过remove清理掉的也就不会出现泄漏。第二种解读,多次复用值改变的问题,其实在调用remove后也不会出现。

而如果ThreadLocal不加static,则每次其所在类实例化时,都会有重复ThreadLocal创建。这样即使线程在访问时不出现错误也有资源浪费。

因此,ThreadLocal一般加static修饰,同时要遵循第一条及时清理。

参考:https://www.cnblogs.com/qiuyong/p/7091689.html
https://zhuanlan.zhihu.com/p/91579723

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值