👨?🎓博主主页:爪哇贡尘拾Miraitow 📆创作时间:🌴2022年2月17日 20:23-22:58🌴 📒内容介绍: ThreadLocal源码分析 📚参考资料:知乎 ?简言以励:列位看官,且将新火试新茶,诗酒趁年华 📝内容较多有问题希望能够不吝赐教🙏 🎃 欢迎点赞 👍 收藏 ?留言 📝
ThreadLocal是什么?
从名字我们就可以看到ThreadLocal 叫做 本地线程变量 ,意思是说,ThreadLocal 中填充的的是当前线程的变量 ,该变量对其他线程而言是封闭且隔离的,ThreadLocal 为变量在每个线程中创建了一个副本 ,这样每个线程都可以访问自己内部的副本变量 。
从字面意思很容易理解,但是实际角度就没那么容易了,作为一个面试常问的点,使用场景也是很丰富。
- 在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
- 线程间数据隔离
- 进行事务操作,用于存储线程事务信息。
- 数据库连接,Session会话管理。
ThreadLocal 来提供线程级别变量,变量只对当前线程可见。相比与使用锁控制共享变量访问顺序 的解决方案ThreadLocal 通过空间换时间 的方案,规避了竞争问题,因为每个线程都有属于自己的变量。
此时就产生了第一个问题:线程如何维护属于自己的变量副本,搞懂了这个也就搞懂了其原理。
举个栗子:
结果演示:
线程:Thread-0,local:Thread-0:0
线程:Thread-1,local:Thread-1:1
线程:Thread-2,local:Thread-2:2
线程:Thread-3,local:Thread-3:3
线程:Thread-4,local:Thread-4:4
线程:Thread-5,local:Thread-5:5
线程:Thread-6,local:Thread-6:6
线程:Thread-7,local:Thread-7:7
线程:Thread-8,local:Thread-8:8
线程:Thread-9,local:Thread-9:9
从结果可以看到,每一个线程都有自己的local 值,这就是TheadLocal的基本使用 。
再举个栗子:
结果演示: 上面是不同的线程之间,那么在同一个线程访问的变量是不是一样的那?
结果演示: 由上面这个栗子可以知道我们知道了同一个线程得到的变量是一样的
ThreadLocal原理
ThreadLocal原理是,每个线程内有一个ThreadLocalMap 类型的成员变量,用来存储资源对象
- 调用set方法,就是以ThreadLocal自己作为key,资源对象作为value,放入当前线程的ThreadLocalMap集合中
- 调用get方法,就是以ThreadLocal自己作为key,到当前线程中查找关联的资源值
- 调用remove方法,就是以ThreadLocal自己作为 key,移除当前线程关联的资源值
key代表的就是Threadlocal对象,不同的线程可以有相同的Threadlocal
源码分析
一、首先看下Thread类
Thread中有一个 threadLocals 属性表示线程的本地变量。这个属性的类型是 ThreadLocal.ThreadLocalMap
ThreadLocalMap是啥?
ThreadLocalMap是 ThreadLocal的内部类 ,他是类 Map结构,也是存储K-V 结构数据,并用 Entry封装 K-V。不同的是 ThreadLocalMap的 Entry的 Key只能是 ThreadLocal类型对象,并且是一个 弱引用 。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
汇总下,ThreadLocalMap 为 ThreadLocal 的一个静态内部类 ,里面定义了Entry 来保存数据,而且是继承的弱引用 。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。
对于每个线程内部有个ThreadLocal.ThreadLocalMap 变量,存取自己的线程变量的时候 ,也是从这个容器中来获取。
ThreadLocal又是啥?
ThreadLocalMap赋值、取值操作的入口在其外部类 ThreadLocal中。
set(v)方法内调用 ThreadLocalMap 的 set(this,v)方法存值。(类似 Map的 put(k,v)方法)
假设我想在桶下标为零的位置上面,再放入一个Threadlocal,因为零的位置已经有别的占用,所以就要找下一个空闲的位置,这就是开放寻址法 ,而不是拉链法
假如我放的key为8,桶下标还是零,put以后就会出现下图情况
get()方法内调用 ThreadLocalMap的 getEntry(this)方法取值(类似 Map的 get(k)方法) 通过代码可以看出:
- 第一次操作线程的 ThreadLocalMap属性时,会初始化一个
ThreadLocal.ThreadLocalMap , set(v)会存入以参数为 Value的 K/V数据, get()会存入以 null为 value的 K/V数据。 - ThreadLocal.ThreadLocalMap 存值操作入口是
ThreadLocal.set(v) 方法,并以当前 ThreadLocal变量为 key,参数为 value。 - ThreadLocal.ThreadLocalMap 取值操作入口是
ThreadLocal.get(v) 方法, key为当前ThreadLocal变量。
就是 Thread用类似 Map的ThreadLocal.ThreadLocalMap 数据结构来存储以 ThreadLocal类型的变量为 Key的数值,并用 ThreadLocal来存取删,操作 ThreadLocalMap 。
- 当我们定义一个 ThreadLocal变量时,其实就是在定义一个 Key
- 当我们调用 set(v)方法时,就是以当前 ThreadLocal变量为 key,传入参数为 value,向 ThreadLocal.ThreadLocalMap存数据
- 当我们调用 get()方法时,就是以当前 ThreadLocal变量为 key,从 ThreadLocal.ThreadLocalMap取对应的数据
为什么key使用弱引用?
如果使用强引用,当ThreadLocal 对象的引用(强引用)被回收了ThreadLocalMap本身依然还持有ThreadLocal的强引用,如果没有手动删除这个key,则ThreadLocal不会被回收,所以只要当前线程不消亡,ThreadLocalMap引用的那些对象就不会被回收,可以认为这导致Entry内存泄漏。
①Thread 可能需要长时间运行( 如线程池中的线程) ,如果key不再使用 ,需要在内存不足(GC )时释放其占用的内存 ②但GC仅是让key的内存释放,后续还要根据key是否为null 来进一步释放值的内存,释放时机有
- 获取key发现null key
- set key时,会使用启发式扫描,清除临近的null key,启发次数与元素个数,是否发现null key有关
- remove时(推荐),因为一般使用ThreadLocal时都把它作为静态变量,因此GC无法回收
强引用:普通的引用,强引用指向的对象不会被回收; 软引用:仅有软引用指向的对象,只有发生gc且内存不足,才会被回收; 弱引用:仅有弱引用指向的对象,只要发生gc就会被回收。
根据上述内容,举个例子
假设现在内存不足,需要GC,就会把弱引用也就是Key进行回收 GC以后,用灰色代表为Null,但是vaule还存在,没有回收,资源没有释放 假如,我们现在要去get一个Key为1的值,因为我们的Key为1,已经没有了,所以我们get这个key的时候就会在key为1的位置上放入当前线程的K,并且让value置为null,这和我们以往的map都不一样,如果看懂前面的代码,这里应该没啥疑问
我们假如想要给当前线程1,添加一个Key为8的变量,这个时候之前的桶下标为8的位置的key,在内存不足的时候已经被回收了
这时候我们再去放入这个key为8时就会出现以下结果 通过上图,我们发现9,10的key也被清理了,但是离的远的14的key并没有被清理,所以我们得到结论:set key时,会使用启发式扫描,清除临近的null key,启发次数与元素个数,是否发现null key有关
ThreadLocal变量为什么用static修饰
根据之前的例子我们可以知道一般使用ThreadLocal时都把它作为静态变量, 为了避免重复创建TSO(thread specific object,即与线程相关的变量。),我们知道,一个ThreadLocal实例对应当前线程中的一个TSO实例。因此,如果把ThreadLocal声明为某个类的实例变量(而不是静态变量),那么每创建一个该类的实例就会导致一个新的TSO实例被创建。显然,这些被创建的TSO实例是同一个类的实例。于是,同一个线程可能会访问到同一个TSO(指类)的不同实例,这即便不会导致错误,也会导致浪费(重复创建等同的对象)!因此,一般我们将ThreadLocal使用static修饰即
所以我们之前的get,set的情况一般不会出现,因为key不会被回收,所以就去主动出击使用remove 假如去清除a
扩 展
ThreadLocalMap的Hash冲突解决办法?
采用线性探测的方式 (也是之前说的开放寻址法),根据 key计算 hash值,如果出现冲突,则向后探测,当到哈希表末尾的时候再从0开始,直到找到一个合适的位置。
这种算法也决定了 ThreadLocalMap不适合存储大量数据 。
二、ThreadLocalMap的扩容问题?
ThreadLocalMap初始大小为 16 ,加载因子为 2/3 ,当 size大于 threshold时,就会进行扩容。
扩容时,新建一个大小为原来数组长度的两倍的数组,然后遍历旧数组中的 entry并将其插入到新的hash数组中,在扩容的时候,会把 key为 null的 Entry的 value值设置为 null,以便内存回收,减少内存泄漏问题 。
参考链接: https://mp.weixin.qq.com/s/ND-nUCGvXTHkEClKqN1qrQ https://zhuanlan.zhihu.com/p/102744180
|