ThreadLocal

是什么

ThreadLocal提供线程局部变量,这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过get和set方法)都有自己的、独立的初始化变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(用户ID或事务ID)与线程关联起来。

能做什么

实现每一个线程都有自己专属的本地变量副本(不和别人共享),主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存副本的值从而避免了线程安全问题。

案例

总公司按照销售的总套数来分配奖金,不关心每人的销售数

分公司按照每个销售所销售的个数来分配奖金,关心每人的销售数——ThreadLocal

规范

必须回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常被复用,如果不清理自定义的ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄露问题。尽量在代理中使用try-finally块回收。

jdk1.8后 可以使用ThreadLocal.withInitial()来初始化。之前使用new ThreadLocal()+重写initialValue()来初始化。

Thread,ThreadLocal,ThreadLocalMap的关系

ThreadLocal是每个线程专属的本地变量副本,各自线程,人手一份。因此,Thread类中,有ThreadLocal。

ThreadLocalMap是ThreadLocal类中的一个静态内部类,本质上是一个以ThreadLocal实例为key,任意对象为value的Entry对象。

当我们为threadLocal变量赋值,实际上就是以当前threadLocal实例为key,值为value的Entry往这个threadLocalMap中放。

JVM内部维护了一个线程版的Map<ThreadLocal,Value>(通过ThreadLocal对象的set方法,结果把ThreadLocal自己当作key,放进了ThreadLoacalMap中),每个线程要用到这个T的时候,用当前线程去Map中获取,通过这样让每个线程都有了自己独立的变量,人手一份,竞争条件清除,在并发模式下是绝对安全的变量。

ThreadLocal的内存泄露问题

为什么ThreadLocalMap中的静态内部类Entry继承自WeakReference弱引用?

什么是内存泄露?

不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。

ThreadLocalMap是经过两层包装的ThreadLocal对象:

  1. 第一层是使用WeakReference<ThreadLocal<?>>将ThreadLocal对象变成一个弱引用对象。
  2. 第二层包装是定义了一个专门的类Entry来扩展WeakRefrence<ThreadLocal<?>>。

强引用(默认)

当内存不足时,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收。

它是最常见的普通对象引用,只要还有强引用指向一个对象,就表示对象还活着,垃圾收集器不会碰这种对象。

把一个对象赋给一个引用变量,这个引用变量就是强引用。当一个对象被强引用变量引用时,它处于科大状态,是不可能被垃圾回收机制回收的,即使该对象与以后永远不会被用到,JVM也不会回收。因此强引用是造成JVM内存泄露的主要原因之一。

对于一个普通对象,如果没有其他的引用关系,只要超过了引用域或者显示地将强引用赋值为null,一般就认为是可以被垃圾收集的了,具体的回收时机还要看垃圾回收策略。

软引用

软引用是是一种相对强引用弱化了一些的引用,需要用SortReference类来实现,可以让对象豁免一些垃圾收集。

当内存充足时不会被回收,当系统内存不足时会被回收。

软引用通常用在对内存敏感的程序中,比如高速缓存。

如果需要读取大量本地图片,每次读取都从硬盘都会影响性能,一次性全部加载又可能内存溢出。此时可以使用软引用:

用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,当内存不足,JVM挥挥手这些缓存图片占用的空间。

弱引用

弱引用需要用WeakReference类来实现,他比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM内存空间是否足够,都会回收该对象占用的内存。

虚引用

  1. 虚引用必须和引用队列联合使用,虚引用不会决定对象的生命周期。如果一个对象仅持有虚引用,那么他就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,她不能单独使用也不能通过它访问对象,必须和引用队列联合使用。
  2. 须有你用的作用是跟踪对象被垃圾回收的状态,仅仅是提供了一种确保对象被finalize以后,做某些事的通知机制。他的get方法总是返回null,因此无法访问引用对象。
  3. 设置虚引用关联对象的唯一目的就是在这个对象被收集器回收的时候,收到一个系统通知或者后续添加进一步的处理,用来实现比finalize机制更灵活的回收操作。

为什么Entry要用弱引用

当方法执行完毕后,栈帧销毁强引用。但此时线程的ThreadLocalMap里某个entry的key还指向这个对象,若这个key的引用时强引用,就会导致key指向的ThreadLocal对象v指向的对象不能被gc回收,造成内存泄露。若这个key的引用是弱引用,就大概率会减少内存泄露的问题。使用弱引用就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null。之后再调用get,set或remove时,就会尝试删除key为null的entry。

弱引用之外的问题

当threadLocal外部强引用被设置为null时,GC根据根可达性分析,这个threadLocal就没有任何一条链路能够到达它,就会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key的value。如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:ThreadRef->Thread->ThreadLocalMap->Entry->value永远无法回收,造成内存泄露。

当然,如果thread运行结束,在垃圾回收时就会被回收。但是在实际中,我们有时会用线程池去维护线程,复用线程是不会结束的,所以threadLocal内存泄露就值得我们小心。

所以,我们要在不使用某个ThreadLocal对象后,手动调用remove方法来删除它。如果不手动调用remove方法,再线程重复使用即这个线程的ThreadLocalMap被复用时,有可能获取到上个线程遗留下来的value,造成bug。

清除脏Entry

在get,set和remove方法中,都会调用expungeStaleEntry方法,删除key为null的Entry。

总结

  1. ThreadLocal.withInitial(()->初始化值) 否则容易空指针异常
  2. 建议把ThreadLocal修饰为static,因为ThreadLocal能实现现成的数据隔离,不在于它本身,而在于Thread的ThreadLocalMap,所以,ThreadLocal可以只初始化一次,只分配一块存储空间就足以了,没必要作为成员变量多次被初始化。
  3. 用完记得手动remove
  • ThreadLocal并不解决线程间共享数据的问题
  • ThreadLocal适用于变量在线程间隔离且在方法间共享的场景
  • ThreadLocal通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
  • 每个线程持有一个只属于自己的转世Map,并维护了ThreadLocal对象与具体实例的映射,该Map由于制备持有他的线程访问,故不存在线程安全以及锁的问题。
  • ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题。
  • get,set和remove方法都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry三个方法回收key为null的Entry对象的值,即为具体实例以及Entry对象本身,从而防止内存泄露,属于安全加固的方法。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值