前段时间在网上看到一篇关于ThreadLocal内存泄漏的文章,觉得很有趣,因此最近抽空看了下ThreadLocal的源码,在这里记录下自己对于ThreadLocal内存泄漏的个人理解。
ThreadLocal的定义和作用这里不写了,网上有很多,这里直奔主题,首先看下ThreadLocal几个比较核心的方法。
GET方法
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
首先是get方法,get的方法实际上是返回当前线程的某个ThreadLocal所对应的值,如果对于当前线程没有该值则调用initialValue方法。该方法首先会获取当前CPU正在执行的线程,然后通过getMap方法获取当前线程的ThreadLocalMap对象,getMap方法及threadLocals如下:
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap类类似于HashMap,底层实际是一个Entry数组,通过将ThreadLocal对象作为key并与ThreadLocalMap的size进行&运算最终获取value在这个Entry数组中的位置。基本原理就是这样这里不做详述。
回到get方法,当该线程的ThreadLocalMap已经被创建,则通过当前的ThreadLocal对象去该map中寻找对应的Entry即键值对,若能够在Entry数组中定位到则返回对应的value若找不到或map不存在则调用setInitialValue方法。setInitialValue方法源码如下:
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
该方法大致的意思是首先将value初始化成某个值,这个值可以通过重写的方法自定义,获取当前线程对应的ThreadLocalMap,如果存在该map则将初始化的value存入到对应的Entry数组的某个index下,若不存在则创建一个新的ThreadLocalMap方法并将value放入,最后返回初始值。createMap代码如下:
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
SET方法
这里介绍下另一个核心方法,set方法,源码如下:
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
set方法也是比价简单,获取当前线程,并找到对应的ThreadLocalMap,若map不存在则创建新的ThreadLocalMap为这个线程并将value存入对应的index到Entry数组中;若存在map则直接存入。
REMOVE方法
remove方法实际上是将该ThreadLocal对象在当前线程对应的ThreadLocalMap中的entry的引用置为null以达到删除该entry的目的。
/**
* Removes the current thread's value for this thread-local
* variable. If this thread-local variable is subsequently
* {@linkplain #get read} by the current thread, its value will be
* reinitialized by invoking its {@link #initialValue} method,
* unless its value is {@linkplain #set set} by the current thread
* in the interim. This may result in multiple invocations of the
* {@code initialValue} method in the current thread.
*
* @since 1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
/**
* Remove the entry for key.
*/
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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
/**
* Clears this reference object. Invoking this method will not cause this
* object to be enqueued.
*
* <p> This method is invoked only by Java code; when the garbage collector
* clears references it does so directly, without invoking this method.
*/
public void clear() {
this.referent = null;
}
ThreadLocal内存泄漏
那么为什么会出现内存泄漏呢,首先看下ThreadLocalMap对于Entry的定义:
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
这里的注释是说这个entry的key也就是ThreadLocal使用了弱引用,对于用弱引用修饰的对象来说,如果没有强引用引用着这个对象,则在下一个GC回收周期到来时就会被GC所回收(具体看这里)。回到上面这段注释的说明,对于entry.get()方法等于null的情况,这说明这个key已经不再存在任何引用,因此这个entry可以从目标数值中移除。
这里我们可以考虑以下场景,当我们使用set方法将某value存入entry数组中,之后将该value对应的key也就是threadLocal对象的强引用去掉,那么当下一个GC回收周期到来之时该threadLocal对象将会被回收,但是对于这个threadLocal对象在entry数组中对应的value来说,它是不会被回收的,只有当我们调用remove方法或者该线程被关闭的时候才会被回收。本来这也是没有问题的,对于一个线程来说,执行完自己的方法就会关闭线程,但是对于一些特定场景,比如该线程是由线程池所维护的,也就是说线程不会被销毁而是继续由线程池所持有,那么因为value不会被回收,那么最终将造成内存泄漏。