ThreadLocal底层实现与实际应用

引言

作用:

   ThreadLocal 对象可以提供线程局部变量,每个 Thread 拥有一份独立的线程上下文,多个线程之间互不干扰。

数据结构:

类图:
在这里插入图片描述

说明:

  1. ThreadLocal.get() 每次通过当前Thread,获取对应的 ThreadLocalMap,实现了不同线程间的数据隔离
  2. 在 ThreadLocalMap 中,key 继承自WeakReference ,是一个弱引用类型,因此需要注意GC时对弱引用的回收问题
  3. 最终的变量是放在当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 内,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。ThreadLocalMap 类似 HashMap的结构,只是HashMap是由 数组+链表/红黑树 实现的,而 ThreadLocalMap 中并没有链表结构。

弱应用问题: GC 之后 key 是否为 null?

   ThreadLocal 的key是弱引用,那么在 ThreadLocal.get() 的时候,然后发生GC之后,key是否是null?

Java的四种引用类型:
强引用: 我们常常 new 出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候
软引用: 使用 SoftReference 修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收
弱引用: 使用 WeakReference 修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收
虚引用: 虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知

结论:

   当get操作在GC前时,方法栈中存在一个强引用执行ThreadLocal,所以ThreadLocal这个key不会回收。

原因:

   从类图中我们可以看到,ThreadLocalMap 中的 key 是一个弱引用的 ThreadLock 对象,如果没有 GC Root 指向(GC可达性算法)创建的 ThreadLocal 对象,那么在 GC 时,这个 Entity.ThreadLocal 将被回收,但 Entity 中的 value 是一个强引用类型的,不会被GC回收,所以便出现 key 被回收,导致 value 永远存在,出现内存泄漏。

内存泄漏是指,GC 不能释放掉已经没有在使用的某一对象的内存空间。

如何避免弱引用key的内存泄漏问题?

保持强引用指向ThreadLocal

   如果在虚拟机栈中,有 GC Root 指向弱引用ThreadLocal(即图中的1.0),那么该对象不会被回收,具体的JVM内存情况如下图:
在这里插入图片描述

为了不让 ThreadLocal 被 GC 回收,可以让一个强引用指向 ThreadLocal,具体的方法:

  1. 在方法执行时,在 stack 中维护一个强引用指向实例化的 ThreadLocal,作为一个GC Root
  2. 使用静态类成员变量,维护一个ThreadLocal 的GC Root.

及时删除ThreadLocalMap中key==null的Entry

  1. 最好的办法就是使用 ThreadLocal 的 remove() 方法来删除数据
  2. 在 ThreadLocal 的 set()、get()方法中,也包含了 replaceStaleEntry() 、rehash() 方法,会将所有 key 为 null 的 Entry 的值设置为 null ,也可以避免内存泄漏。

父子线程中,ThreadLocal 数据如何共享?

   JDK 提供了 InheritableThreadLocal 类,来完成指定场景下的数据复制动作。

   其实现原理是在父线程中,通过调用 new Thread() 方法来创建子线程,Thread#init 方法在 Thread 的构造方法中被调用,在 init 方法中拷贝父线程数据到子线程中,具体代码在(Thread:418)。但一般异步化处理都是使用的线程池,而 InheritableThreadLocal 是在 new Thread 中的 init() 方法完成赋值的,而线程池是线程复用的逻辑,所以这个类适用的面不够。

   因此,跨线程上下文信息的复制,可以使用共享内存块的思路来实现,即将上下文赋值到一个类的属性上,子线程初始化完成后,在用这个类属性赋值到当前线程中,完成上下文的拷贝。具体案例可参考digest日志-异步任务中 digest 的复制

   开源组件 TransmittableThreadLocal 也可以解决这个问题,具体的可以自己去研究。

应用场景

ThreadLocal与AOP方式实现读写分离的整体实现思路:

  1. 拦截所有service层中所有方法,并通过方法名,去标记使用哪一个库

  2. 通过继承AbstractRoutingDataSource重写determineCurrentLookupKey,来确定每一个线程要使用的数据源。(由于不同的线程,需要不同的数据源,所以使用ThreadLocal来标记库名,保证库名的线程安全问题)

GitHub源码(线上源码,package:cn.edu.cdcas.partyschool.aspect)

基于 ThreadLocal 建设的 biz digest log

实现方案与细节 参考

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值