ThreadLocal的概念
Thread和ThreadLocal是绑定的, ThreadLocal依赖于Thread去执行, Thread将需要隔离的数据存放到ThreadLocal(准确的讲是ThreadLocalMap)中, 来实现多线程处理。
Thread、ThreadLocal关系
引用关系
Thread -> ThreadLocalMap -> entry -> (key,Object)
ThreadLocal被垃圾回收后,在ThreadLocalMap里对应的Entry的键值会变成null,但是Entry是强引用,那么Entry里面存储的Object,并没有办法进行回收,所以ThreadLocalMap 做了一些额外的回收工作。
// java/lang/Thread.java
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
// ThreadLocal -> ThreadLocalMap
// 继承WeakReference,key是弱引用 value 强引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
弱引用导致内存泄漏,为什么key不设置为强引用
key是threadlocal,把这个设置为弱引用,就是为了防止用户threadlocal=null的时候,key内存泄露,这个是为了解决key的内存泄露,但是(null,value)的情况存在。
如果key设置为强引用,当threadLocal实例释放后,threadLocal=null, 但是threadLocal会有强引用指向threadLocalMap,threadLocalMap.Entry又强引用threadLocal,这样会导致threadLocal不能正常被GC回收。
弱引用虽然会引起内存泄漏, 但是也有set、get、remove方法操作对null key进行额外擦除操作。
// java/lang/ThreadLocal.java -> ThreadLocalMap
// 获取threadLocal值
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
// key没有直接定位到值, 调用 getEntryAfterMiss -> expungeStaleEntry
// getEntryAfterMiss()方法,在这个方法中,如果k == null , 则调用expungeStaleEntry(i)
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
// key=null,执行清理操作
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
// key=null的Entry,value进行赋空值,释放空间避免内存泄漏,等待GC
// 重新hash,知道遇到Entry=null
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
set 是在rehash的时机进行额外回收工作
线程执行结束后会不会自动清空Entry的value
当currentThread执行结束后, threadLocalMap GC不可达从而被回收,Entry等也就都被回收了。但是项目中经常使用线程池,所以currentThread一般不会处于终止状态。
实践
在不使用的情况下,主动调用remove方法进行数据清理。
引用类型介绍
| 引用类型 | GC回收时间 | 用途 | 生存时间 |
|---|---|---|---|
| 强引用 | never | 对象的一般状态 | JVM停止运行时 |
| 软引用 | 内存不足 | 对象缓存 | 内存不足时终止 |
| 弱引用 | GC时 | 对象缓存 | GC后终止 |
| 虚引用 | unknown | unknown | unknown |