☆* o(≧▽≦)o *☆嗨~我是小奥
个人博客:小奥的博客
CSDN:个人CSDN
Github:传送门
面经分享(牛客主页):传送门
文章作者技术和水平有限,如果文中出现错误,希望大家多多指正!
如果觉得内容还不错,欢迎点赞收藏关注哟! ❤️
ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例时(通过get和set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段。使用它的目的是希望将状态(例如用户ID或者事务ID)与线程关联起来。这些变量分配在堆内的TLAB中。
ThreadLocal的使用非常简单,只需要在每个线程中调用set()
方法来存储数据,然后在需要的时候调用get()
方法来获取数据。在多线程环境下,每个线程都拥有自己的ThreadLocal实例,因此可以独立地存储和访问自己的数据,从而避免了线程安全问题。
ThreadLocal 实例通常来说都是 private static
类型的,属于一个线程的本地变量,用于关联线程和线程上下文。每个线程都会在 ThreadLocal 中保存一份该线程独有的数据,所以是线程安全的。
ThreadLoacl作用:
对比 synchronized:
synchronized | ThreadLocal | |
---|---|---|
原理 | 同步机制采用以时间换空间的方式, 只提供了一份变量,让不同的线程排队访问 |
ThreadLocal 采用以空间换时间的方式, 为每个线程都提供了一份变量的副本,从而实现同时访问而相不干扰 |
侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间的数据相互隔离 |
常用方法:
方法 | 描述 |
---|---|
get() | 返回当前线程的此线程局部变量副本中的值 |
set(T value) | 将当前线程的此线程局部变量的副本设置为指定的值 |
remove() | 删除此线程局部变量的当前线程的值 |
initialValue() | 返回此线程局部变量的当前线程的“初始值” |
withInitial(Supplier extends S>) | 创建线程局部变量 |
阿里巴巴规范约定:必须回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常会复用,如果不清理自定义的ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄漏等问题。尽量在代理中使用try-finally块进行回收。
ThreadLocal.ThreadLocalMap
)ThreadLocalMap实际上就是一个以ThreadLocal实例为key,任意对象为value的Entry对象。当我们为ThreadLocal变量赋值,实际上就是以当前ThreadLocal实例为key,值为value的Entry往这个ThreadLocalMap中存放。
返回该线程局部变量的初始值。
protected T initialValue() {
return null;
}
获取当前线程与当前ThreadLocal对象相关联的线程局部变量。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null) {
// 以当前的 ThreadLocal 为 key,调用 getEntry 获取对应的存储实体 e
ThreadLocalMap.Entry e = map.getEntry(this);
// 对 e 进行判空
if (e != null) {
// 获取存储实体 e 对应的 value值
T result = (T)e.value;
return result;
}
}
/*有两种情况有执行当前代码
第一种情况: map 不存在,表示此线程没有维护的 ThreadLocalMap 对象
第二种情况: map 存在, 但是【没有与当前 ThreadLocal 关联的 entry】,就会设置为默认值 */
// 初始化当前线程与当前 threadLocal 对象相关联的 value
return setInitialValue();
}
private T setInitialValue() {
// 调用initialValue获取初始化的值,此方法可以被子类重写, 如果不重写默认返回 null
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// 判断 map 是否初始化过
if (map != null)
// 存在则调用 map.set 设置此实体 entry,value 是默认的值
map.set(this, value);
else
// 调用 createMap 进行 ThreadLocalMap 对象的初始化中
createMap(t, value);
// 返回线程与当前 threadLocal 关联的局部变量
return value;
}
public void set(T value) {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的 ThreadLocalMap 对象
ThreadLocalMap map = getMap(t);
// 判断 map 是否存在
if (map != null)
// 调用 threadLocalMap.set 方法进行重写或者添加
map.set(this, value);
else
// map 为空,调用 createMap 进行 ThreadLocalMap 对象的初始化。参数1是当前线程,参数2是局部变量
createMap(t, value);
}
// 获取当前线程 Thread 对应维护的 ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
// 创建当前线程Thread对应维护的ThreadLocalMap
void createMap(Thread t, T firstValue) {
// 【这里的 this 是调用此方法的 threadLocal】,创建一个新的 Map 并设置第一个数据
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
public void remove() {
// 获取当前线程对象中维护的 ThreadLocalMap 对象
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
// map 存在则调用 map.remove,this时当前ThreadLocal,以this为key删除对应的实体
m.remove(this);
}
ThreadLocalMap 是 ThreadLocal 的内部类,没有实现 Map 接口,用独立的方式实现了 Map 的功能,其内部 Entry 也是独立实现。
ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。
// 初始化当前 map 内部散列表数组的初始长度 16
private static final int INITIAL_CAPACITY = 16;
// 存放数据的table,数组长度必须是2的整次幂。
private Entry[] table;
// 数组里面 entrys 的个数,可以用于判断 table 当前使用量是否超过阈值
private int size = 0;
// 进行扩容的阈值,表使用量大于它的时候进行扩容。
private int threshold;
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
// this.referent = referent = key;
super(k);
value = v;
}
}
添加数据,ThreadLocalMap使用线性探测法来解决哈希冲突。
该方法会一直探测下一个地址,直到有空的地址后插入,若插入后 Map 数量超过阈值,数组会扩容为原来的 2 倍。
假设当前 table 长度为16,计算出来 key 的 hash 值为 14,如果 table[14] 上已经有值,并且其 key 与当前 key 不一致,那么就发生了 hash 冲突,这个时候将 14 加 1 得到 15,取 table[15] 进行判断,如果还是冲突会回到 0,取 table[0],以此类推,直到可以插入,可以把 Entry[] table 看成一个环形数组。
线性探测法会出现堆积问题,可以采取平方探测法。
在探测过程中ThreadLocal会复用key为null的脏Entry对象,并进行垃圾清理,防止出现内存泄漏。
private void set(ThreadLocal<?> key, Object value) {
// 获取散列表
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
// 哈希寻址
int i = key.threadLocalHashCode & (len-1);
// 使用线性探测法向后查找元素,碰到 entry 为空时停止探测
for (ThreadLocal.ThreadLocalMap.Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
// 获取当前元素 key
ThreadLocal<?> k = e.get();
// ThreadLocal 对应的 key 存在,【直接覆盖之前的值】
if (k == key) {
e.value = value;
return;
}
// 【这两个条件谁先成立不一定,所以 replaceStaleEntry 中还需要判断 k == key 的情况】
// key 为 null,但是值不为 null,说明之前的 ThreadLocal 对象已经被回收了,当前是【过期数据】
if (k == null) {
// 【碰到一个过期的 slot,当前数据复用该槽位,替换过期数据】
// 这个方法还进行了垃圾清理动作,防止内存泄漏
replaceStaleEntry(key, value, i);
return;
}
}
// 逻辑到这说明碰到 slot == null 的位置,则在空元素的位置创建一个新的 Entry
tab[i] = new Entry(key, value);
// 数量 + 1
int sz = ++size;
// 【做一次启发式清理】,如果没有清除任何 entry 并且【当前使用量达到了负载因子所定义,那么进行 rehash
if (!cleanSomeSlots(i, sz) && sz >= threshold)
// 扩容
rehash();
}
// 获取【环形数组】的下一个索引
private static int nextIndex(int i, int len) {
// 索引越界后从 0 开始继续获取
return ((i + 1 < len) ? i + 1 : 0);
}
// 在指定位置插入指定的数据
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
// 获取散列表
Entry[] tab = table;
int len = tab.length;
Entry e;
// 探测式清理的开始下标,默认从当前 staleSlot 开始
int slotToExpunge = staleSlot;
// 以当前 staleSlot 开始【向前迭代查找】,找到索引靠前过期数据,找到以后替换 slotToExpunge 值
// 【保证在一个区间段内,从最前面的过期数据开始清理】
for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// 以 staleSlot 【向后去查找】,直到碰到 null 为止,还是线性探测
for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
// 获取当前节点的 key
ThreadLocal<?> k = e.get();
// 条件成立说明是【替换逻辑】
if (k == key) {
e.value = value;
// 因为本来要在 staleSlot 索引处插入该数据,现在找到了i索引处的key与数据一致
// 但是 i 位置距离正确的位置更远,因为是向后查找,所以还是要在 staleSlot 位置插入当前 entry
// 然后将 table[staleSlot] 这个过期数据放到当前循环到的 table[i] 这个位置,
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// 条件成立说明向前查找过期数据并未找到过期的 entry,但 staleSlot 位置已经不是过期数据了,i 位置才是
if (slotToExpunge == staleSlot)
slotToExpunge = i;
// 【清理过期数据,expungeStaleEntry 探测式清理,cleanSomeSlots 启发式清理】
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// 条件成立说明当前遍历的 entry 是一个过期数据,并且该位置前面也没有过期数据
if (k == null && slotToExpunge == staleSlot)
// 探测式清理过期数据的开始下标修改为当前循环的 index,因为 staleSlot 会放入要添加的数据
slotToExpunge = i;
}
// 向后查找过程中并未发现 k == key 的 entry,说明当前是一个【取代过期数据逻辑】
// 删除原有的数据引用,防止内存泄露
tab[staleSlot].value = null;
// staleSlot 位置添加数据,【上面的所有逻辑都不会更改 staleSlot 的值】
tab[staleSlot] = new Entry(key, value);
// 条件成立说明除了 staleSlot 以外,还发现其它的过期 slot,所以要【开启清理数据的逻辑】
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
private static int prevIndex(int i, int len) {
// 形成一个环绕式的访问,头索引越界后置为尾索引
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
ThreadLocal 的 get 方法以当前的 ThreadLocal 为 key,调用 getEntry 获取对应的存储实体 e。
private Entry getEntry(ThreadLocal<?> key) {
// 哈希寻址
int i = key.threadLocalHashCode & (table.length - 1);
// 访问散列表中指定指定位置的 slot
Entry e = table[i];
// 条件成立,说明 slot 有值并且 key 就是要寻找的 key,直接返回
if (e != null && e.get() == key)
return e;
else
// 进行线性探测
return getEntryAfterMiss(key, i, e);
}
// 线性探测寻址
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
// 获取散列表
Entry[] tab = table;
int len = tab.length;
// 开始遍历,碰到 slot == null 的情况,搜索结束
while (e != null) {
// 获取当前 slot 中 entry 对象的 key
ThreadLocal<?> k = e.get();
// 条件成立说明找到了,直接返回
if (k == key)
return e;
if (k == null)
// 过期数据,【探测式过期数据回收】
expungeStaleEntry(i);
else
// 更新 index 继续向后走
i = nextIndex(i, len);
// 获取下一个槽位中的 entry
e = tab[i];
}
// 说明当前区段没有找到相应数据
// 【因为存放数据是线性的向后寻找槽位,都是紧挨着的,不可能越过一个 空槽位 在后面放】,可以减少遍历的次数
return null;
}
触发一次全量清理,如果数组长度大于等于长度的1/2
,则进行resize。
private void rehash() {
// 清楚当前散列表内的【所有】过期的数据
expungeStaleEntries();
// threshold = len * 2 / 3,就是 2/3 * (1 - 1/4)
if (size >= threshold - threshold / 4)
resize();
}
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
// 【遍历所有的槽位,清理过期数据】
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
Entry 数组为扩容为原来的 2 倍 ,重新计算 key 的散列值,如果遇到 key 为 null 的情况,会将其 value 也置为 null,帮助 GC。
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
// 新数组的长度是老数组的二倍
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
// 统计新table中的entry数量
int count = 0;
// 遍历老表,进行【数据迁移】
for (int j = 0; j < oldLen; ++j) {
// 访问老表的指定位置的 entry
Entry e = oldTab[j];
// 条件成立说明老表中该位置有数据,可能是过期数据也可能不是
if (e != null) {
ThreadLocal<?> k = e.get();
// 过期数据
if (k == null) {
e.value = null; // Help the GC
} else {
// 非过期数据,在新表中进行哈希寻址
int h = k.threadLocalHashCode & (newLen - 1);
// 【线程探测】
while (newTab[h] != null)
h = nextIndex(h, newLen);
// 将数据存放到新表合适的 slot 中
newTab[h] = e;
count++;
}
}
}
// 设置下一次触发扩容的指标:threshold = len * 2 / 3;
setThreshold(newLen);
size = count;
// 将扩容后的新表赋值给 threadLocalMap 内部散列表数组引用
table = newTab;
}
删除Entry。
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
// 哈希寻址
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
// 找到了对应的 key
if (e.get() == key) {
// 设置 key 为 null
e.clear();
// 探测式清理
expungeStaleEntry(i);
return;
}
}
}
(1)内存泄漏产生:ThreadLocal 内存泄漏的原因通常是因为ThreadLocalMap 是 Thread的一个属性, ThreadLocal 的生命周期和线程的生命周期一样长。而Entry将ThreadLocal作为key,值作为value对象,它继承自WearReference,并且在构造函数中调用了super()方法,所以ThreadLocal对象的key是一个弱引用,而value是一个强引用。
当GC回收ThreadLocal时,弱引用的key会被回收,但是强引用的value不会被回收,就会造成内存泄漏。
主要有两个原因:
关于第一点,只要在使用完ThreadLocal,手动调用其remove方法删除对应的Entry,就能避免内存泄漏。
关于第二点,由于ThreadLocalMap是Thread的一个属性,被当前线程所引用,所以ThreadLocalMap的生命周期和线程一样。如果ThreadLocal变量被回收,那么当前线程的ThreadLocal变量副本所指向的key就是null,即Entry的结构为(null,value),那么这个Entry对应的value永远无法被访问到,而value还存在线程的强引用,只有在线程退出以后,value的强引用才会断开。
(2)如何解决内存泄漏
(3)为什么不把Key设置为强引用
如果当ThreadLocalMap的key为强引用,当垃圾回收时,由于ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除的话,那么ThreadLocal不会被回收,导致Entry内存泄漏。
通过使用 WeakReference 来包装 ThreadLocal 实例,可以让 ThreadLocal 实例变为弱引用,当没有其他强引用指向 ThreadLocal 实例时,ThreadLocal 实例就会被垃圾回收。因此,使用 WeakReference 可以避免 ThreadLocal 实例的内存泄漏。
由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。当key为null,在下一次ThreadLocalMap调用set()、getEntry()、remove()方法的时候会通过线性探测法堆key进行判断,如果key为null(即ThreadLocal为null)则会对Entry进行垃圾回收。所以使用弱引用比强引用多一层保障,就算不调用 remove,也有机会进行 GC。