ThreadLocal的原理和内存泄漏问题分析
ThreadLocal概述
ThreadLocal意思是线程本地类,该变量对其他变量是隔离的
ThreadLocal的使用场景
当某些变量只希望自己的线程获取,变量值只需要在当前线程中存取时,就可以考虑使用ThreadLocal
- 存储用户信息:使用Token登陆时,可以使用Interceptor拦截器,请求达到前,先根据Token判断用户是否登录,同时就可以把用户信息存储到ThreadLocal中,进入业务后,通过ThreadLocal来取,每个用户都有自己的线程,所以用户只能取出来自己的用户信息。
ThreadLocal原理分析
原来的设计:
对每个ThreadLocal开辟一个Map,线程要存储数据时,可以使用Thread的id当key,数据为value,这样就可以分离不同的线程。
新版的设计:
- 每个线程都有自己的ThreadLocalMap对象,ThreadLocal对象为key,线程变量副本为value
- ThreadLocal用来维护每个线程的ThreadLocalMap对象
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. 这是Thread的源码,默认为NUll*/
ThreadLocal.ThreadLocalMap threadLocals = null;
- ThreadLocalMap是ThreadLocal的静态内部类
版本的版本:
- 每个Entry存储的Entry数量变少,本来是每个线程都需要一个Entry存储,尽量避免了哈希冲突的发生
- 当线程销毁后,内部的ThreadLocalMap也会消失,而以Thread id为key的还需要手动删除
ThreadLocal源码分析
set()
设置value到ThreadLocalMap中,key就是这个ThreadLocal对象
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取线程内的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null)
// 已经有map,这是k-v是 ThreadLocal-设置的数据
map.set(this, value);
else
// 变量默认是null,这里初始化
createMap(t, value);
}
get()
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// 直接从对应的Thread中获取ThreadLocalMap的Entry,key就是this,value就是要的值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果不存在数据就返回初始值,初始值就是null,初始值可以被子类重写
return setInitialValue();
}
remove()
就拿出来ThreadLocalMap后删除this(ThreadLocal对象)
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreaLocalMap源码分析
总体来说ThreadLocal的代码不难,ThreadLocal的难点在ThreadLocalMap这个静态内部类
类定义,并没有继承或实现Map接口
// 并没有实现Map接口,而是自己实现了一个
static class ThreadLocalMap {
// 内部的Entry也是自己实现的并且继承了弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
// 构造方法super(k) 来让ThreadLocal对象被WeakReference封装
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
.....
}
......
}
成员变量,类似于HashMap
// 初始容量
private static final int INITIAL_CAPACITY = 16;
// Entry数组,长度必须是2的幂次,懒加载
private Entry[] table;
// 实际的数量
private int size = 0;
// 扩容的门槛
private int threshold;
Entry为什么继承弱应用
WeakReference修饰的类当GC时就会被回收,目的是将ThreadLocal的生命周期和线程的生命周期分离
Entry中key如果强引用会造成内存泄漏么?
很多人对此有误解
手画图示
如果只有一个线程,ThreadLocal不使用之后,堆中的ThreadLocal也无法释放,因为TheadLocalMap中一个Entry的key是它的强引用,这时候就无法访问到这个Entry,造成了内存泄漏(虽然可以通过ThreadLocalMap通过地址来访问,但这个地址无意义,除非你提前存储了它,所以这种内存泄漏我认为是类似内存泄漏),所以强弱引用和内存泄漏无关,主要是这种设计造成的内存泄漏
只要线程不结束,ThreadLocalMap无法回收,强引用指向ThreadLocal,也不会被回收
源码中,key为弱引用
如果是弱引用,ThreadLocal的引用消失后,就只剩下ThreadLocalMap中的key的弱引用指向ThreadLocal,当触发GC时,该引用就会被回收掉,这时ThreadLocal不在GC ROOT,也就会被回收,此时的Entry的key为null,value还存在,只要线程还存在,就不会释放Map,这个key为null,value为数据的Entry永远无法被访问。这就导致了内存泄漏
内存泄漏总结
导致内存泄漏的主要原因是->ThreadLocalMap的生命周期和Thread一样长
避免内存泄漏的方式:
- 使用完后立即remove()
- 使用完后线程也结束了
都会导致内存泄漏,为什么使用弱引用呢?
GC发生时,内存泄漏位置key = null,ThreadLocalMap在执行set/get方法时,会对key为null进行判断,key为null,就会把value也设置为null,比强引用多了一层保障
ThreadLocalMap的哈希冲突解决
不做解释了,使用的是线性探测法解决的哈希冲突,相当于把Map当成环形数组,一直向右探测