随笔篇-ThreadLocal原理分析

1. 存取值分析

1.0 前言

ThreadLocal存取值都是借助ThreadLocalMap对象去进行存取值,而ThreadLocalMap类是定义在ThreadLocal类中的一个静态内部类

image-20211110093901448

在再存取值时都是获取当前线程的ThreadLocalMap实例,即每个线程都会创建一个ThreadLocalMap类型对象,如下:

image-20211110094526201

该类型为实例变量,也就是说每个线程对象都会拥有一个threadLocalMap对象

ThreadLocalMap本质就是一个就是Map类型,在其内部也维护了一个Entry类型对象,如下:

image-20211110095259806

这样就与HashMap类似,存储的也是key-value类型数据

1.1 存值

从上文的表述中,存储值都是通过ThreadLocalMap进行操作,根据源码可知,主要分为三个步骤:

  • 获取当前线程
  • 获取ThreadLocalMap类型对象
  • 判断是否为空,为空创建,否则存值

关于每个步骤详细解释如下:


获取当前线程

image-20211110100009387

根据线程实例获取ThreadLocalMap类型对象

image-20211110100034078

而获取ThreadLocalMap对象则需要传入一个线程对象,查看getMap源码可知,其实就是返回了线程对象的ThreadLocalMap实例对象

image-20211110100213242

而通过之前的1.0章节我们知道,线程对象里面的threadLocals对象并没有实例化,只是给了null

因此当第一次获取threadLocals这个实例时,最后的结果肯定为空


实例对象为空则创建ThreadLocalMap类型对象

从其源码可知,当实例对象为空的时候,就回去创建ThreadLocalMap,创建该类型对象需要传入当前线程对象,以及要存储的值,如下:

image-20211110100534933

查看createMap源码可知,就是new了一个ThreadLocalMap对象

  • key值为this也就是当前的ThreadLocal对象
  • value 为具体要存入ThreadLocal的值

最后将创建好的对象赋值给 线程对象里面的ThreadLocalMap实例,源码如下:

image-20211110101020896

这样在一个线程中始终只有一个ThreadLocalMap类型对象,但是这个类型中可以存放多个ThreadLocal对象值


如果 map不为空,则将值存入ThreadLocalMap中,key为当前的ThreadLocal对象

image-20211110101317189

上述步骤其流程图如下:

image-20211110101710534

可能这里图看的并不清晰,可以查看: 高清大图

1.2 取值

ThreadLocal取值原理与存值原理相似,都是几个固定步骤,如下:

  • 获取当前线程对象
  • 根据线程对象获取ThreadLocalMap实例
  • 如果ThreadLocalMap实例不为空,则获取值
  • 如果ThreadLocalMap实例为空,则调用setInitValue方法

关于这几个步骤详细解释如下


获取当前线程对象

根据线程对象获取ThreadLocalMap实例

image-20211110104401332

这两个步骤在上文中进行了详细阐述,这里就不再过多赘述


如果ThreadLocalMap实例不为空,则获取值

这里取值并不是直接从Map中获取value,而是根据this(当前ThreadLocal)对象取获取Entry实体

如果Entry实体不为空,则获取Entry的value,如下:

image-20211110105009595

如果ThreadLocalMap实例为空,则调用setInitValue方法

从源码可知,当ThreadLocalMap实例为空时,则调用setInitValue方法,而该方法

则又是先调用了initialValue方法获取value值,也就是存储要初始化存储的值

接下来就又是存值的几个步骤,源码如下:

image-20211110105344631

通过查看initiaValue方法可只,该方法为返回null

image-20211110105509348

但是如果子类重写了该方法,返回并不是null,那么map中存储的就不是null值


关于上述步骤流程图如下:

image-20211110110125607

流程图不太清晰,如果想要查看清晰,可以通过该网址查看:高清大图

1.3 总结

从上文的表述中知道,往ThreadLocal中存值一共有两种方式

  • 直接创建ThreadLocal对象,并且通过set方式进行存值
  • 或者子类继承ThreadLocal,然后重写initialValue方法

当然取值就只通过get方法了,一个线程从始至终就只有一个ThreadLocalMap对象

而一个ThreadLocalMap可以存储多个ThreadLocal,即一个线程可以有多个ThreadLocal对象

当然一个ThreadLocal对象只能存值一个实例对象,并不能存储多个实例,原因在上文中也进行了阐述

2. 内存泄漏

2.0 前言

当一个线程启动,并且给创建了一个ThreadLocal对象,在堆栈中内存简图如下:

image-20211110135040042

当发生内存泄漏时,有可能时ThreadLocalMap里面的key发生泄漏,也有可能是Map发生value发生泄漏,因此接下来来详细分析一下泄漏原因

2.1 分析

通过源码可知,在ThreadLocalMap中,可以发现Entry继承了WeakReference,且在其构造方法中,将key通过调用父类方法进行了弱引用处理,如下:

image-20211110135814132

弱引用是指这个如果对象的引用是一个弱引用,那么当下一次发生GC的时候一定会被回收掉

从源码知道,也就是ThreadLocalMap中的ThreadLocal对象一旦回收掉,也就是说key就为null了。

但是value确实一个强引用,即ThreadLocalMap中存在 key为null的value,如下:

image-20211110140440478

如果当前线程对象,执行一个耗时操作,长时间不被销毁,也就意味着ThreadLocalMap中key为value的数据,永远没法被GC,因此可能发生内存泄漏(OOM)

2.2 解决

JDK在设计时已经考虑到这些问题,例如当调用set(),remove(),rehash()方法会手动清理key为null的数据,例如查看set()方法调用链可知,最后调用了resize()方法,如下:

image-20211110142915770

resize方法中,会主动的将key为null的数据也制空,以便防止内存泄漏,如图:

image.png

因此如果当ThreadLocal发生内存泄漏了,那么肯定是存在了key为null的数据,所以在阿里开发规范中, 提到了如果ThreadLocal使用完成,那么应该手动调用一下remove

image-20211110144008399

思考:不是已经做了优化了么,怎么还会存在key为null的数据呢?

其实当一个ThreadLocal不再被被使用,那么resize方法就不会调用,如果线程不停止,那么就会发生OOM有可能

你可能感兴趣的:(随笔篇-ThreadLocal原理分析)