工作中使用ThreadLocal&笔记

265 阅读7分钟

工作中使用ThreadLocal

  • 上线文缓存,在智慧防疫服务端中,每个医护登录系统每个请求链路以及用户信息都放入上下文缓存中,相互隔离互不影响。
  • 利用ThreadLocal装载自己的SimpleDateFormat对象,达到线程安全的目的。

一 简介

1.1 问题

ThreadLocal中ThreadLocalMap数据结构和关系?

ThreadLocal的key是弱引用,这是为什么?

ThreadLocal内存泄漏问题你知道吗?

ThreadLocal中最后为什么要加remove方法?

1.2 是什么

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

1.3 能干嘛

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

1.4 代码总结

因为每个Thread内有自己的实例副本且该副本只由当前线程自己使用,不存在多个线程间共享的问题。

如何不争抢

  • 加入synchronized或者Lock控制资源的访问顺序
  • 人手一份,大家各自安好

二 阿里ThreadLocal规范开始

1.1 非线程安全的SimpleDateFormat

SimpleDateFormat中的日期格式不是同步的,SimpleDateFormat类内部有一个Calendar对象引用,它用来存储和这个SimpleDateFormat相关的日期信息,如果SImpleDateFormat是个static的,那么多个thread之间就会共享这个SimpleDateFormat,同时也是共享这个Calendar引用。

1.2 解决

  • 将SimpleDateFormat定义成局部变量,这样做的缺点是每调用一次方法就会创建一个SimpleDateFormat对象,方法结束又要作为垃圾回收。

  • 线程本地变量ThreadLocal

  • 加锁

三 ThreadLocal源码分析

Thread,ThreadLocal ThreadLocalMap

ThreadLocalMap实际上就是一个以ThreadLocal实例为key,任意对象为value的entry对象。

当我们为ThreadLocal变量赋值,实际上就是以当前ThreadLocal实例为key,值为value的entry往这个map里面放。

四 内存泄漏

4.1 阿里手册

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

4.2 什么是内存泄漏

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

4.3 强引用、软引用、弱引用、虚引用

4.3.1 强引用

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

强引用是我们最常见的普通对象引用。只要还有强引用指向一个对象,就能表明对象还活着,垃圾收集器不会碰这种对象。在Java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用,即使对象以后永远都不会用到JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一。

4.3.2 软引用

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

对于只有软引用的对象来说,当系统内存充足时,它不会被回收。

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

4.3.3 弱引用

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

4.3.4 虚引用

虚引用需要java.lang.ref.PhantomReference类来实现。就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。

设置虚引用关联的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理。

4.4 关系

  • 每个Thread对象维护着一个ThreadLocalMap的引用

  • ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储

  • 调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值Value是传递进来的对象

  • 调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象

  • ThreadLocal本身并不存储值,它只是自己作为一个key来让线程从ThreadLocalMap获取value,正因为这个原理,所以ThreadLocal能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响。

4.5 为什么要用弱引用

4.5.1 为什么源代码要用弱引用

  • 当function01方法执行完毕后,栈帧摧毁强引用tl也就没有了。但此时线程的ThreadLocalMap里某个entry引用还是指着这个对象

  • 若这个key引用是强引用,就会导致key指向的ThreadLocal对象以及v的对象不能被gc回收,造成内存泄漏

  • 若这个key引用是弱引用就大概率会减少泄漏的问题(还有一个key为null的雷),使用弱引用,就可以使ThreadLocal对象在方法执行完毕顺利被回收且Entry的key引用指向为null

4.5.2 弱引用不是万事大吉

  • 当我们为threadLocal变量赋值,实际上就是当前的Entry(threadLocal实例为key,值为value)往这个threadLocalMap中存放。Entry中的key是弱引用,当threadLocal外部强引用被置为null(tl=null),那么系统 GC 的时候,根据可达性分析,这个threadLocal实例就没有任何一条链路能够引用到它,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

  • 当然,如果当前thread运行结束,threadLocal,threadLocalMap,Entry没有引用链可达,在垃圾回收的时候都会被系统进行回收。

  • 但在实际使用中我们有时候会用线程池去维护我们的线程,比如在Executors.newFixedThreadPool()时创建线程的时候,为了复用线程是不会结束的,所以threadLocal内存泄漏就值得我们小心

4.5.3 set、get方法会去检查所有键为null的entry对象

  • 都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法清理掉key为null的脏entry。
  • 用完记得remove

五 总结

  • ThreadLocal 并不解决线程间共享数据的问题

  • ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景

  • ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题

  • 每个线程持有一个只属于自己的专属Map并维护了ThreadLocal对象与具体实例的映射,该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题

  • ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题

  • 都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏,属于安全加固的方法