ThreadLocal一般用来在同一个线程中传递参数。不同线程中的ThreadLocal参数是隔离的。那么TheadLocal是否会导致内存泄漏呢?我们一起来分析。
一、ThreadLocal、ThreadLocalMap、Thread源码分析
1.ThreadLocal中有个静态内部类ThreadLocalMap
2.ThreadLocalMap中又有个静态内部类Entry,Entry继承WeakReference<ThreadLocal<?>>类,说明Entry是一个弱引用(如果只有弱引用存在,那么在GC的时候就会被回收)。Entry的源码如下
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry的构造方法的有两个参数,可以看出来一个是ThreadLoca<?>类型的k,传给了父类WeakReference
3.在ThreadLocalMap中定义了一个Entry[]数组类型的变量table。源码如下:
private Entry[] table;
4.在ThreadLocalMap的构造方法中对table变量进行了初始化,源码如下
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);
}
从以上源码可以看出,在构造ThreadLocalMap对象的时候做了如下几件事
- table进行初始化,初始化大小为16;
- 计算ThreadLocal<?>变量firstKey在数组中的位置i
- 根据firstKey和 firstValue构造一个Entry对象,并赋值给table[i]。
- 设置size为1
- 计算一个门槛值
5.ThreadLocalMap的set方法源码如下:
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
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)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
从以上源码可以看出,在set方法中做了如下几件事
-
计算ThreadLocal<?>变量firstKey在数组中的位置i
-
进入循环操作:判断数组中的位置i是否已经创建Entry对象,
- 如果已经创建Entry对象
- 先判断已经创建的Entry对象的key是否等于当前的传入的ThreadLocal<?>对象,如果等于,则直接结束set方法
- 再判断已经创建的Entry的key是否为null,如果为空则根据set方法的参数重新创建Entry对象赋值给table[i],并且直接结束set方法
- 以上都不是,则根据i和len计算下一个i的值,并获取新的table[i],赋值给对象e,重新进入上面的循环
- 如果当前的table[i]没有创建Entry对象,则退出循环逻辑
- 如果已经创建Entry对象
-
根据set方法的参数创建Entry对象赋值给table[i]
-
根据firstKey和 firstValue构造一个Entry对象,并赋值给table[i]。
-
设置size++
-
判断是否进行Entry数组的扩容
6.TheadLocal中set方法源码如下
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//从当前线程中获取 ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
//如果map不为空则添加当前的threadLocal对象和value
map.set(this, value);
else
//如果map为空则为当前线程t,初始化ThreadLocalMap对象
createMap(t, value);
}
从以上源码可以看出,set方法做了如下几件事
-
获取当前线程t;
-
从当前线程中获取 ThreadLocalMap类型的threadLocals属性,从getMap方法的代码可以看出,Thread类中有个ThreadLocalMap类型的属性threadLocals,但是ThreadLocalMap这个类型是定义在了ThreadLocal中的,是它的静态内部类。getMap方法的代码如下
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
在Thread类中可以看到threadLocals属性的类型为ThreadLocal.ThreadLocalMap
/* ThreadLocal values pertaining to this thread. This map is maintained * by the ThreadLocal class. */ ThreadLocal.ThreadLocalMap threadLocals = null;
-
如果map不为空则添加当前的ThreadLocal对象this和传入的value进map数组中。ThreadLocalMap的set方法在上面的第5点已经说明。
-
如果map为空则调用createMap方法,从createMap的代码可以看出就是给传入的线程t初始化threadLocals属性,ThreadLocalMap的构造方法在上面第4已经说明。代码如下
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
7.TheadLocal中get方法源码如下
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//根据当前的ThreadLocal对象获取map中的Entry对象
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//返回Entry的value
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
从以上源码可以看出,在get方法做了如下几件事
- 获取当前线程;
- 获取当前线程的ThreadLocalMap
- 根据当前的ThreadLocal对象获取map中的Entry对象
- 返回Entry的value
根据以上源码分析可知,ThreadLocal类中其实没有存任何属性用来存数据,所有的数据都是存在当前线程Thread中threadLocals属性中,threadLocals是ThreadLocal.ThreadLocalMap类型。ThreadLocal.ThreadLocalMap类对象定义在了ThreadLocal类中,并且是它的一个静态内部类。ThreadLocal.ThreadLocalMap类似一个HashMap,其中的Key 是一个弱引用的ThreadLocal<?>类型,value是当前方法传入的值。我们平常操作ThreadLocal中的set和get方法操作其实是操作当前线程的threadLocals属性。
二、结论
1.如果TreadLocal作为局部变量使用(现实情况中很少当局部变量用),三者在JVM中的关系图如下
当threadLocal局部变量使用完出栈之后,图例如下
从上图可以看到堆中的threadLocal 对象会只存在虚引用,当GC的时候它就会被回收掉。所以当ThreadLocal当作局部变量使用的话,严格意义上不会存在内存泄漏。因为只要GC堆中的ThreadLocal就会被回收。
2.如果TreadLocal作为静态变量使用,静态变量存在方法区,那么上面的图会有一个从方法区的变量实线连接到堆中的ThreadLocal内存对象,大家脑补一下图。堆中的ThreadLocal对象会一直被静态变量所引用,就不会被回收。此时也不会存在内存泄漏。
3.如果TreadLocal在线程池中使用
- 如果该线程属于核心线程,它不会被销毁。当ThreadLocal对象在当前线程的业务使用完成之后,不调用remove方法,那么当前线程的threadLocals中的ThreadLocal和Value就不会被删除和回收。该线程回到线程池中后,当下一个业务使用线程池再次使用到之前的线程时,通过ThreadLocal会拿到之前使用该线程的业务参数。可能会导致业务参数混用。严格来说这种情况是一种内存泄漏。所以这种情况下需要手动remove。
- 如果该线程不属于核心线程,它可能会被销毁。如果不被销毁那么和上面的结论一样。如果被销毁了,那么线程中的threadLocals会设置为null。此时也不存在内存泄漏。
- 业务结束之后都需要手动调用一下remove。