1. 存取值分析
1.0 前言
ThreadLocal
存取值都是借助ThreadLocalMap
对象去进行存取值,而ThreadLocalMap
类是定义在ThreadLocal
类中的一个静态内部类
在再存取值时都是获取当前线程的ThreadLocalMap
实例,即每个线程都会创建一个ThreadLocalMap
类型对象,如下:
该类型为实例变量,也就是说每个线程对象都会拥有一个
threadLocalMap
对象
而ThreadLocalMap
本质就是一个就是Map类型,在其内部也维护了一个Entry类型对象,如下:
这样就与HashMap
类似,存储的也是key-value类型数据
1.1 存值
从上文的表述中,存储值都是通过ThreadLocalMap
进行操作,根据源码可知,主要分为三个步骤:
- 获取当前线程
- 获取
ThreadLocalMap
类型对象 - 判断是否为空,为空创建,否则存值
关于每个步骤详细解释如下:
获取当前线程
根据线程实例获取ThreadLocalMap
类型对象
而获取ThreadLocalMap
对象则需要传入一个线程对象,查看getMap
源码可知,其实就是返回了线程对象的ThreadLocalMap
实例对象
而通过之前的1.0
章节我们知道,线程对象里面的threadLocals
对象并没有实例化,只是给了null
值
因此当第一次获取
threadLocals
这个实例时,最后的结果肯定为空
实例对象为空则创建ThreadLocalMap
类型对象
从其源码可知,当实例对象为空的时候,就回去创建ThreadLocalMap
,创建该类型对象需要传入当前线程对象,以及要存储的值,如下:
查看createMap
源码可知,就是new
了一个ThreadLocalMap
对象
- key值为
this
也就是当前的ThreadLocal
对象 - value 为具体要存入
ThreadLocal
的值
最后将创建好的对象赋值给 线程对象里面的ThreadLocalMap
实例,源码如下:
这样在一个线程中始终只有一个
ThreadLocalMap
类型对象,但是这个类型中可以存放多个ThreadLocal
对象值
如果 map
不为空,则将值存入ThreadLocalMap
中,key为当前的ThreadLocal
对象
上述步骤其流程图如下:
可能这里图看的并不清晰,可以查看: 高清大图
1.2 取值
ThreadLocal
取值原理与存值原理相似,都是几个固定步骤,如下:
- 获取当前线程对象
- 根据线程对象获取
ThreadLocalMap
实例 - 如果
ThreadLocalMap
实例不为空,则获取值 - 如果
ThreadLocalMap
实例为空,则调用setInitValue
方法
关于这几个步骤详细解释如下
获取当前线程对象
根据线程对象获取ThreadLocalMap
实例
这两个步骤在上文中进行了详细阐述,这里就不再过多赘述
如果ThreadLocalMap
实例不为空,则获取值
这里取值并不是直接从Map中获取value,而是根据this(当前ThreadLocal
)对象取获取Entry
实体
如果Entry
实体不为空,则获取Entry的value
,如下:
如果ThreadLocalMap
实例为空,则调用setInitValue
方法
从源码可知,当ThreadLocalMap
实例为空时,则调用setInitValue
方法,而该方法
则又是先调用了initialValue
方法获取value值,也就是存储要初始化存储的值
接下来就又是存值的几个步骤,源码如下:
通过查看initiaValue
方法可只,该方法为返回null
但是如果子类重写了该方法,返回并不是null,那么map中存储的就不是null值
关于上述步骤流程图如下:
流程图不太清晰,如果想要查看清晰,可以通过该网址查看:高清大图
1.3 总结
从上文的表述中知道,往ThreadLocal
中存值一共有两种方式
- 直接创建
ThreadLocal
对象,并且通过set方式进行存值 - 或者子类继承
ThreadLocal
,然后重写initialValue
方法
当然取值就只通过get方法了,一个线程从始至终就只有一个ThreadLocalMap
对象
而一个ThreadLocalMap
可以存储多个ThreadLocal
,即一个线程可以有多个ThreadLocal
对象
当然一个ThreadLocal
对象只能存值一个实例对象,并不能存储多个实例,原因在上文中也进行了阐述
2. 内存泄漏
2.0 前言
当一个线程启动,并且给创建了一个ThreadLocal
对象,在堆栈中内存简图如下:
当发生内存泄漏时,有可能时ThreadLocalMap里面的key发生泄漏,也有可能是Map发生value发生泄漏,因此接下来来详细分析一下泄漏原因
2.1 分析
通过源码可知,在ThreadLocalMap
中,可以发现Entry继承了WeakReference
,且在其构造方法中,将key通过调用父类方法进行了弱引用处理,如下:
弱引用是指这个如果对象的引用是一个弱引用,那么当下一次发生
GC
的时候一定会被回收掉
从源码知道,也就是ThreadLocalMap
中的ThreadLocal
对象一旦回收掉,也就是说key就为null了。
但是value确实一个强引用,即ThreadLocalMap
中存在 key为null的value,如下:
如果当前线程对象,执行一个耗时操作,长时间不被销毁,也就意味着ThreadLocalMap
中key为value的数据,永远没法被GC
,因此可能发生内存泄漏(OOM)
2.2 解决
JDK
在设计时已经考虑到这些问题,例如当调用set()
,remove()
,rehash()
方法会手动清理key为null的数据,例如查看set()
方法调用链可知,最后调用了resize()
方法,如下:
在resize
方法中,会主动的将key为null的数据也制空,以便防止内存泄漏,如图:
因此如果当ThreadLocal
发生内存泄漏了,那么肯定是存在了key
为null的数据,所以在阿里开发规范中, 提到了如果ThreadLocal使用完成,那么应该手动调用一下remove
思考:不是已经做了优化了么,怎么还会存在key为null的数据呢?
其实当一个
ThreadLocal
不再被被使用,那么resize方法就不会调用,如果线程不停止,那么就会发生OOM
有可能