ThreadLocal的内部结构
JDK8之前的设计是这样的:
每一个ThreadLocal都创建一个Map,然后用线程作为Map的key,要存储的局部变量作为Map的value,这样就能达到给个线程的局部变量隔离的效果。这是最简单的设计方法,JDK最早期的ThreadLocal确实是这样设计的,但现在已经不是了。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-76zffSKh-1667883713834)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221106210315090.png)]](https://1000bd.com/contentImg/2024/04/27/346bb4a74aea2a41.png)
现在的设计是这样的:
ThreadLocal的设计是:每一个Thread维护一个ThreadLocalMap,这个Map的key是ThreadLocal实例本身,value才是真正要存储的值Object。
具体过程:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ElM3NItH-1667883713835)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221106210633811.png)]](https://1000bd.com/contentImg/2024/04/27/b8a3c855a7c34262.png)
这样设计的好处:
ThreadLocal的核心方法源码
除了构造方法外,ThreadLocal对外暴露的方法有以下4个:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LWacbqMg-1667883713836)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221106212921663.png)]](https://1000bd.com/contentImg/2024/04/27/68f6ab84d35a99e7.png)
set方法
/**
* 设置当前线程对应的ThreadLocal的值
* @Param value 将要保存在当前线程对应的ThreadLocal的值
*/
public void set(T value) {
//获取当前线程对象
Thread t = Thread.currentThread();
//获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
//判断map是否存在
if (map != null)
//存在则调用map.set设置此实体entry
map.set(this, value);
else
// 1)当前线程Thread不存在ThreadLocalMap对象
// 2)则调用createMap进行ThreadLocalMap对象的初始化
// 3)并将t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadlocalMap中
createMap(t, value);
}
/**
* 获取当前线程Thread对应维护的ThreadLocalMap
*
* @Param t 当前线程
* @return 对应维护的ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
* 创建当前线程Thread对应维护的ThreadLocalMap
*
* @Param t 当前线程
* @Param firstValue 存放map中第一个entry的值
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
总结:
get方法:
/**
*返回当前线程中保存ThreadLocal的值
*如果当前线程没有此ThreadLocal变量
*则它会通过{@link #initialValue}方法进行初始化值
*
*@return返回当前线程对应ThreadLocal的值
*/
public T get() {
//获取当前线程对象
Thread t = Thread.currentThread();
//获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
//如果此map存在
if (map != null) {
//已当前的ThreadLocal为Key,调用getEntry获取对应的存储实体e
ThreadLocalMap.Entry e = map.getEntry(this);
//对e进行判空
if (e != null) {
@SuppressWarnings("unchecked")
//获取存储实体e对应的value值
//即为我们想要的当前线程对应此ThreadLocal的值
T result = (T)e.value;
return result;
}
}
/*
初始化:有两种情况有执行当前代码
第一种情况:map不存在,表示此线程没有维护的ThreadLocalMap对象
第二种情况:map存在,但没有与当前ThreadLocal关联的entry
*/
return setInitialValue();
}
/**
* 初始化
*
* @return the initial value 初始化后的值
*/
private T setInitialValue() {
//调用initialValue获取初始化的值
//此方法可以被子类重写,如果不重写默认返回null
T value = initialValue();
//获取当前线程对象
Thread t = Thread.currentThread();
//获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
//判断map是否存在
if (map != null)
//存在则调用map.set设置此实体entry
map.set(this, value);
else
// 1)当前线程Thread 不存在ThreadLocalMap对象
// 2)则调用createMap进行ThreadLocalMap对象的初始化
// 3)并将t(当前线程)和value(t对应的值)作为第一个entry放进至ThreadLocalMap中
createMap(t, value);
//返回设置的值value
return value;
}
代码执行流程:
A.首先获取当前线程,根据当前线程获取一个Map
B.如果获取Map为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的Entry e,否则转到D
C.如果e不为null,则返回e.value,否则反转到D。
D.Map为空或者e为空,则通过inItialValue函数获取初始化值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map
remove方法:
/**
*删除当前线程中保存的ThreadLocal对应的实体Entry
*
*/
public void remove() {
//获取当前线程对象中维护的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
//如果此map存在
if (m != null)
//存在则调用map.remove
//以当前ThreadLocal为Key删除对应的实体Entry
m.remove(this);
}
代码执行流程:
A.首先获取当前线程,并根据当前线程获取一个Map
B.如果获取的Map不为空,则移除当前ThreadLocal对象对应的Entry
initialValue方法
/**
* 返回当前线程对应的ThreadLocal的初始化值
*
* 此方法的第一次调用发生在,当线程通过get方法访问此线程的ThreadLocal值时
* 除非先调用了set方法,在这种情况下,initialValue才不会被这个线程调用。
* 通常情况下,每个线程最多调用一次这个方法。
*
* 这个方法仅仅简单的返回null{@code null};
* 如果程序员想ThreadLcoal线程局部变量有一个除null以外的初始值
* 必须通过子类继承{@code ThreadLocal}的方式去重写此方法
* 通常,可以通过匿名内部类的方式实现
*/
public T initialValue(){
return null;
}
此方法的作用是返回该线程局部变量的初始值
ThreadLocalMap源码分析
基本结构:
ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也是独立实现。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T6UudAqd-1667883713836)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221108103738702.png)]](https://1000bd.com/contentImg/2024/04/27/844b9c25cbb34b3c.png)
成员变量
/**
* 初始化 --必须是2的整次幂
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* 存放数据的table,Entry类的定义在下面分析
* 同样,数组长度必须是2的整次幂
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
/**
* 数组里面Entrys的个数,可以用于判断table当前使用量是否超过阈值
* The number of entries in the table.
*/
private int size = 0;
/**
* 进行扩容的阈值,表使用量大于它的时候进行扩容
* The next size value at which to resize.
*/
private int threshold; // Default to 0
跟HashMap类似,INITIAL_CAPACIty代表这个Map的初始容量;table是一个Entry类型的数组,用于存储数据;size代表表中的存储数目;threshold代表需要对应size的阈值。
存储结构-Entry
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
在ThreadLocalMap中,也是用Entry来保存K-V结构数据的,不过Entry中的key只能是ThreadLocal对象,这点在构造方法中已经限定死了。
另外,Entry继承WeakReference,也就是key(ThreadLocal)是弱引用,其目的是ThreadLocal对象的生命周期和线程生命周期解绑。
弱引用和内存泄漏
在使用ThreadLocal的过程中会发现有内存泄漏的情况发生,就猜想这个内存泄漏跟Entry中使用了弱引用的key有关系。这个理解其实是不对的。
Memory overflow:内存溢出,没有足够的内存提供申请者使用。
Memory leak:内存泄漏是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出。
2.弱引用相关概念
Java中的引用有4种类型:强,软,弱,虚当前这个问题主要涉及到强引用和弱引用:
强引用("String"Reference),就是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还"活着",垃圾回收器就不会回收这种对象
弱引用(WeakReference),垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
如果key使用强引用
假设ThreadLocalMap中的key使用了强引用,那么会出现内存泄漏吗?
此时ThreadLocal的内存图(实线表示强引用):
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sfWiwmK4-1667883713837)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221108114244907.png)]](https://1000bd.com/contentImg/2024/04/27/e94d7b52e91fd236.png)
假设在业务代码中使用完ThreadLocal,threadLocalRef被回收。
但因为threadLocalMap的Entry强引用了threadLocal,造成threadLocal无法被回收。
在没有手动删除这个Entry已经CurrentThread依然运行的前提下,始终有强引用链threadRef->currentThread->threadLocalMap->entry; entry就不会被回收(Entry中包括了ThreadLocal实例和Value),导致Entry内存泄漏。
也就是说,ThreadLocalMap的key使用了强引用,是无法完全避免内存泄漏的。
如果key使用弱引用
假设ThreadLocalMap中的key使用了弱引用,那么会出现内存泄漏吗?
此时ThreadLocal的内存图(实线表示弱引用):
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ip5ziPyB-1667883713837)(C:\Users\20745\AppData\Roaming\Typora\typora-user-images\image-20221108115341954.png)]](https://1000bd.com/contentImg/2024/04/27/b9bc97467db2b68f.png)
1.同样假设在业务代码中使用完ThreadLocal,threadLocalRef被回收了。
2.由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向threadLocal实例,所以threadLocal就可以顺利被gc回收,此时Entry中的Key=null
3.但是在没有手动删除这个entry以及CurrentThread依然运行的前提下,也存在有强引用链threadRef->currentThread->threadLocalMap->entry->value, value不会被回收,而这块value永远不会被访问到了,导致value内存泄漏。
也就说,ThreadLocalMap中的key使用了弱引用,也有可能内存泄漏。
总结比较以上两种情况,我们就会发现,内存泄漏的发生跟ThreadLocalMap中的key是否使用弱引用是没有关系的。
在2种内存泄漏的情况中,都有两个前提:
第一点很好理解,只要在使用完ThreadLocal,调用其remove方法删除对应的Entry,就能避免内存泄漏。
第二点:由于ThreadLocalMap是Thread的一个属性,被当前线程所引用,所以它的生命周期跟Thread一样长。那么在使用完ThreadLocal的使用,如果当前Thread也随之执行结束,ThreadLocalMap自然也会被gc回收,从根源上避免了内存泄漏。
综上,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏。在set与getEntry方法中会对key为null进行判断,如果为null,那么是会对value置为null的。所以弱引用比强引用多一层保障。