前言
本文将从ThreadLocal的作用以及源码来进行对其的详细分析,包括内存泄漏以及其的本质原因等等
首先是ThreadLocal能干啥的问题,我将其总结为三点
1.传递数据:实现在公共组件中传递信息的需求
2.线程隔离:实现每个线程都是不相互影响的
3.线程并发:在多线程场景下完成需求
常用方法
常用的就是四个方法
1.构造方法
2.set方法
将变量绑定到线程上
3.get方法
获取绑定的线程
4.remove方法
移除绑定的变量(entry)
结构分析
我们这里直接考虑jdk8之后的结构而不考虑之前的结构了
这里是每一个thread线程持有一个threadlocalmap
是一个map结构,里面存放的是一个个entry
entry的key是threadlocal对象
entry的值是线程绑定的变量值
这样设计的优势:
1. 每个map存储的entry数量变少了 因为之前是由thread的数量决定的 现在的entry是由threadlocal决定
2.当thread销毁的时候threadlocalmap也会随之销毁,减少内存的使用
ThreadLocal和synchronized的区别与联系
我们知道解决多线程的并发问题,最常见的思想就是加锁
我们很轻易的就能想到Synchronized关键字
但是使用synchronized关键字加锁会导致程序的性能降低
主要的是两者的思想不同
synchronized主要思想使用时间换空间,侧重于多线程程序的同步性
threadlocal主要是用空间换时间
主要侧重于线程之间的隔离
一个简单的案例分析
假设我们这里需要做一个经典的转账接口,我们故意设置a扣费之后进行报错,使得b不能收到转账,这样a的转账就不翼而飞了,我们希望使用这两个操作来解决这个问题
思路1:
使用事务和加锁来解决问题
我们将这转账一系列操作使用事务来完成,但是我们就得注意这里服务分层了
业务层和数据层之间的连接必须要保持一致,这样就多了一个传参以及加锁的性能损耗
思路2:
使用threadlocal来操作
这样既不需要多付出性能的代价,也降低的层之间的耦合性,虽然增加了空间损耗,但是我们知道对于现在来说空间是比较不值钱的,相对来说效果更优.
源码分析
下面我们对几个常用方法进行源码的解析
set方法
先获取线程对应的threadlocalmap
获取到了就直接设置值
获取不到就创建一个新的map并设置初始值
get方法
首先获取当前线程,根据当前线程获取一个map
如果map不为空,就根据threadlocal找到对应的entry返回value,
如果map为空则通过initialValue函数获取初始值value
然后根据threadlocal的引用和value创建一个新的map
remove方法
如果发现对应的entry,流程是先获取到thread对应的threadlocalmap,存在就移除entry
不存在就啥也不干
ThreadLocalMap的分析
threadlocalMap是threadlocal的一个静态内部类
threadlocalmap是threadlocal独立实现的,没有实现任何的map接口等,以下是他的结构图
他的entry也是自己定义的,没有继承或者实现某些接口
下面我们来看看entry的内部定义源码
我们发现这里的entry是实现的弱引用,为什么要使用弱引用呢,我们在后面继续聊
内存泄露
首先我们谈谈内存泄露的定义
这里大致有两种定义
一种是内存溢出(用户分配的内存的空间不够用)的原因
可能可以使用调大设置内存空间来解决问题
另一种是系统的总内存不足以使用(满足不了程序的需求)
网上有想法说:内存泄露是因为使用若引用的缘故
其实不然,我们下面开始分别谈谈使用弱引用和强引用之间的区别,最后谈谈内存泄露的真实原因到底是什么.
强引用 被强引用引用的时候,不会被gc回收
弱引用 被弱引用引用的时候,被gc发现即回收
使用弱引用
这里如果我们不使用remove方法,只要线程还是存在的,这里指向entry的引用就会存在,entry仍然不会删除
虽然这里的threadlocal被置空了,但是也只是threadlocal被回收了,而entry不会被回收
强引用
仍然是始终有强引用链指向entry,不会被系统gc回收
本质原因
其本质原因主要和引用的强弱与否用处不大
本质上是thread和entry的生命周期是一样的,使用弱引用相对来说更加有保障一点
因为在调用get set方法等的时候,如果发现这里的entry为空就会自动给置空
这样可以一定程序上防止内存泄露的问题
解决哈希冲突
我们知道threadlocalmap是一个map本质上还是会出现hash冲突的情况
那么我们会以什么方式来解决hash冲突的呢?
我们从源码上来分析问题
这里的hash计算就是上述代码中i的计算
我们发现他还有一个函数和一个INITIAL_CAPICITY
下面我们再往里面走一步看看
这里使用了原子类的getAndAdd方法每次获取并加上一个十六进制的值
这个值主要和斐波那契数列那个黄金分割值有关,目的是让hash值均匀的分布在2的n次方的数组中,可以用来解决hash冲突问题
这里还与上了一个capacity-1其目的是让其最后生成的hash值不会溢出
因为下面使用的是线性探测的方式来解决hash冲突