一、使用 ThreadLocal 需要注意什么问题
ThreadLocal 操作不当会引发内存泄漏。
原因:内部类 ThradLocalMap 中 Entry 的设计。Entry 继承了 WeakReference<ThreadLocal<?>>,即 Entry 的 key 是弱引用,所以 key 会在垃圾回收的时候被回收掉, 而 key 对应的 value 则不会被回收, 这样会导致一种现象: key 为 null,value 有值。
导致结果:value 会一直累积,最终内存泄漏,GC 无法回收。
// java 8 中 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;
}
}
二、如何解决内存泄漏问题
每次使用 ThreadLocal 之后手动调用 remove() 方法清除数据。remove() 方法会将当前的 key 和 value(Entry) 清除。
/**
* 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) {
// 清除 key
e.clear();
// 清除 value
/*
* Expunge a stale entry by rehashing any possibly colliding entries
* lying between staleSlot and the next null slot. This also expunges
* any other stale entries encountered before the trailing null.
*/
expungeStaleEntry(i);
return;
}
}
}
三、JDK开发者是如何避免内存泄漏的
ThreadLocal的设计者也意识到了这一点(内存泄漏), 他们在一些方法中埋了对key=null的value擦除操作。
这里拿ThreadLocal提供的get()方法举例,它调用了ThreadLocalMap#getEntry()方法,对key进行了校验和对null key进行擦除。
/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*/
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
// else 中方法依次调用,最终擦除 value 值
// java.lang.ThreadLocal.ThreadLocalMap#getEntryAfterMiss
// java.lang.ThreadLocal.ThreadLocalMap#expungeStaleEntry
expungeStaleEntry 方法解释
expungeStaleEntry(i) 方法完成了对 key=null 的 key 所对应的 value 进行赋空, 释放了空间避免内存泄漏。
同时它遍历下一个key为空的entry, 并将value赋值为null, 等待下次GC释放掉其空间。
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;
// 遍历下一个 key 为 null 的 Entry,并将 value 指向 null
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;
}
同理,其他避免内存泄露的方式也是如此,最终都会调用到 expungeStaleEntry。
也只能说尽可能避免内存泄漏, 但并不会完全解决内存泄漏这个问题。比如极端情况下我们只创建ThreadLocal但不调用set、get、remove方法等。所以最能解决问题的办法就是用完ThreadLocal后手动调用remove().
四、手动释放ThreadLocal遗留存储?怎么去设计/实现?
手动释放,其实就是手动调用 remove() 。
我们可以借助 aop ,根据 bean 方法的执行方式,在其执行完成之后调用。
问题思考1:弱引用导致内存泄漏,那为什么key不设置为强引用?
如果 key 设置为强引用,当 threadLocal 实例释放之后,threadLocal = null, 但是 threadLocal 会有强引用指向 ThreadLocalMap,ThreadLocalMap.Entry 又有强引用指向 ThredLocal,这样会导致 threadLocal 不能正常被 GC 回收。
弱引用虽然会引起内存泄漏, 但是也有set、get、remove方法操作对null key进行擦除的补救措施, 方案上略胜一筹。
问题思考2:线程执行结束后会不会自动清空Entry的value?
当currentThread执行结束后, threadLocalMap变得不可达从而被回收,Entry等也就都被回收了,但这个环境就要求不对Thread进行复用,但是项目中经常会复用线程来提高性能, 所以currentThread一般不会处于终止状态。
五、Thread和ThreadLocal有什么联系?
Thread和ThreadLocal是绑定的, ThreadLocal依赖于Thread去执行, Thread将需要隔离的数据存放到ThreadLocal(准确的讲是ThreadLocalMap)中, 来实现多线程处理。
六、Spring如何处理Bean多线程下的并发问题
在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域,因为Spring对一些Bean中非线程安全状态采用ThreadLocal进行处理,解决线程安全问题。
ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。同步机制采用了“时间换空间”的方式,仅提供一份变量,不同的线程在访问前需要获取锁,没获得锁的线程则需要排队。而ThreadLocal采用了“空间换时间”的方式。
ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。