ThreadLocal
基本介绍
介绍:
ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问时,保证各个线程的变量,相对独立于其他线程内的变量,Thread类的实例通常来说是private static类型的,用于关联线程与线程上下文。
作用:
提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个组件之间公共变量传递的复杂度。
总结:
- 线程并发:在多线程并发的场景下
- 传递数据:我们可以通过ThreadLocal在同一线程内,不同组件中传递公共变量
- 线程隔离:每个线程的变量都是线程独立的,不会相互影响。
ThreadLocal类结构
public set(T t):将变量绑定到当前线程中
public public T get():获取当前线程绑定的变量
public setInitialValue():获取当前线程变量的初始值
public remove():移除设置的一个ThreadLocal对象
protected T initialValue():默认返回null,子类可重写的方法,未set()先get()时,此方法会被调用。
ThreadLocal底层存储设计
jdk8中ThreadLocal的设计:每一个Thread维护一个ThreadLocalMap,这个map的key是threadLocal实例本身,value才是真正要存储的值Object。
- 每一个Thread线程内部都有一个Map(ThreadLocalMap)
- map里面存储ThreadLocal对象,和线程的变量副本(value)。
- Thread内部的map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
- 不同线程每次在获取副本值时,别的线程不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
- 这样设计的优点:
- 每一个map存储的entry数量变少,减少hash冲突
- Thread销毁时ThreadLocalMap也会随之销毁,减少内存的使用。
ThreadLocalMap底层分析
ThreadLocalMap是ThreadLocal的内部类,没有实现map接口,用独立的方式实现了map功能,其内部的Entry也是独立实现的。底层数组初始容量为16,临界值2/3,二倍扩容
-
构造方法
首先创建了一个长度为16的Entry数组,然后计算出firstKey的索引,存储到table中,再设置size,临界值。
//构造方法(ThreadLocal,Value)
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//初始化table表,实际上是Entry类型的数组。初始大小为16
table = new Entry[INITIAL_CAPACITY];
//计算要索引位置
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
//存储
table[i] = new Entry(firstKey, firstValue);
//表示map中存储的值多少
size = 1;
//设置临界值2/3
setThreshold(INITIAL_CAPACITY);
}
-
set(key,value)方法
a.先计算出传入的key值的索引位置,此位置没有值就直接创建Entry对象插入,size加1,
之后,调用cleanSomeSlots()方法清理key值为空的Entry对象。若调用此方法后没有清理任何Entry对象,且map中存储值达到了临界值就会进行rehash()的调用。
b.有值,进入for循环,若两值的key相等,用新value替换旧value,退出此方法。
若此位置的key为空,表示这是一个过期元素,用新元素替换过期元素,退出此方法。(此处进行了垃圾清理动作防止内存泄漏。即调用cleanSomeSlots()方法)
for循环的方式:Entry[] table可以看作是一个环形数组,一直向后查找,直到找到空位置,或key相等的值
ThreadLocalMap使用线性探测法解决哈希冲突的。Entry[] table可以看作是一个环形数组。
该方法一次探测下一个地址,直到有空的地址后插入,整个空间都找不到空余的地址,则会产生溢出。
举例:假设当前table的长度为16,若计算出来的索引值为14,且table[14]上已经有值,且其key值与当前值不一样就产生了hash冲突,这时判断table[15],若还是冲突,就继续判断table[0],以此类推直到可以插入。
-
cleanSomeSlots()方法:将key为空,entry不为空的值的value,entry都置为空。
-
rehash()方法:先进行一次全表扫描清理,清理过期的Entry。清理之后长度大于等于原长度的1/2就会进行2倍扩容。resize()方法的调用,
ThreadLocal内存泄露的根本原因:
threadLocalMap的生命周期与Thread一样长,如果没有手动删除对应的key就会导致内存泄漏。
ThreadLocal与Entry之间使用弱引用的原因:
因为在ThreadLocalMap中的set/getEntry方法中会对key进行判断是否为空,为空就将其对应的value也置为空。
当前线程仍在运行的前提下,程序员忘记调用remove()时,弱引用比强引用多了一层保障。当没有强引用引用ThreadLocal之后,只有弱引用的ThreadLocal将被回收掉,这时Entry中的key为空,在下一次ThreadLocalMap调用set/getEntry方法时会将value置为空,回收掉。从而避免内存泄漏。
ThreadLocal与Synchronized的区别:
Synchronized | ThreadLocal | |
---|---|---|
原理 | 同步机制采用以时间换空间的方式,只提供了一份变量,让不同的线程排队访问 | ThreadLocal采用以空间换时间的方式,为每一个线程都提供了一份变量的副本,从而实现同时访问而互不干扰 |
侧重点 | 多个线程之间访问线程同步 | 多线程中让每个线程之间的数据相互隔离 |