背景 #
在之前的探讨中,我们已经了解了如何使用ThreadLocal。接下来,我们将深入探究为什么在实际使用中ThreadLocal无法及时释放内存,必须等到线程结束后才能释放,以及ThreadLocal中的弱引用到底起到了什么作用。
ThreadLocal底层实现原理 #
ThreadLocal能够让每个线程获取到自己独有的值,其实现原理其实相当简单。每个Thread线程都有一个ThreadLocal.ThreadLocalMap threadLocals
变量。从数据类型上我们可以看出,这是一个Map结构。
为什么需要使用Map呢?如果我们只有一个ThreadLocal变量,那么只需要让线程持有一个ThreadLocal的对象引用即可。但实际应用中,我们通常会创建多个ThreadLocal变量:
ThreadLocal<?> threadLocalA = new ThreadLocal<>();
ThreadLocal<?> threadLocalB = new ThreadLocal<>();
为了快速获取线程持有的不同ThreadLocal变量,就需要使用Map结构。这个ThreadLocalMap与普通的HashMap有所不同,我们将在后续文章中详细介绍它的独特实现。本文重点讨论ThreadLocalMap中弱引用的使用方式。
在ThreadLocalMap中,key就是ThreadLocal对象本身,value则是与这个ThreadLocal关联的值。ThreadLocal使用了一个特殊的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继承了WeakReference类,并在初始化时将key和value都存储进去。这里的WeakReference就是我们要重点讨论的弱引用类。
弱引用的作用 #
Java中有多种引用类型,我们最常用的是强引用。例如:
Object obj = new Object();
这里的obj就是一个强引用。只有当obj被赋值为null且没有被其他任何引用时,new Object()占用的内存才会被释放。
而弱引用则不同,它告诉垃圾回收器:虽然我引用了这个对象,但如果这个对象没有被其他强引用指向,你就可以回收它。我们可以通过一个简单的例子来验证这一点:
public class EntryWeakRefTest {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("Entry finalize");
}
}
public static void main(String[] args) {
// 1. 创建ThreadLocal的强引用
ThreadLocal<?> threadLocal = new ThreadLocal<>();
Object value = new Object();
// 2. 创建Entry实例(弱引用关联ThreadLocal)
Entry entry = new Entry(threadLocal, value);
// 3. 检查Entry的key和value是否存活
System.out.println("强引用存在时,Entry的Key存活? " + (entry.get() != null));
System.out.println("Value存活? " + (entry.value != null));
// 4. 移除ThreadLocal的强引用
threadLocal = null;
value = null;
// 5. 触发GC(弱引用此时应被回收)
System.gc();
try { Thread.sleep(500); } catch (InterruptedException e) {}
// 6. 再次检查Entry的key和value
System.out.println("强引用移除后,Entry的Key存活? " + (entry.get() != null));
System.out.println("Value存活? " + (entry.value != null));
entry = null;
// 7. 触发GC,entry也被回收,调用finalize
System.out.println("开始gc");
System.gc();
try { Thread.sleep(500); } catch (InterruptedException e) {}
System.out.println("结束");
}
}
程序输出结果为:
强引用存在时,Entry的Key存活? true
Value存活? true
强引用移除后,Entry的Key存活? false
Value存活? true
开始gc
Entry finalize
结束
从结果可以看出,当我们将threadLocal设置为null时,entry.get()获取到的key就变为null了。这说明当threadLocal被设置为null或结束生命周期时,弱引用指向的对象会被回收。然而,即使我们将value也设置为null,entry.value仍然能获取到值。
这是因为在Entry中,value是一个强引用。只要entry对象存在,value引用的对象就不会被回收。这就是为什么很多资料都建议在使用完ThreadLocal后一定要调用remove方法,否则可能导致内存泄漏。
ThreadLocal如何解决弱引用中的强引用问题 #
既然Entry强引用了value,那么弱引用还有什么作用呢?从上面的测试代码可以看出,弱引用能够告诉我们:当引用值为null时,说明ThreadLocal变量已经被回收了。因此,当线程在调用get、remove或set方法时,ThreadLocalMap会检查哪些Entry的key为null,并将这些Entry清除。
核心代码如下:
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 清除staleSlot处的entry
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// 继续检查后续的entry
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 {
// 重新计算hash位置
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
关键部分在于:
if (k == null) {
e.value = null;
tab[i] = null;
size--;
}
这段代码会在发现key为null时,将对应的Entry清除,从而释放value占用的内存。
总结 #
然而,在实际应用中,我们常常会这样声明ThreadLocal变量:
private static final ThreadLocal<byte[]> threadLocal = new ThreadLocal<>();
这种情况下,ThreadLocal变量是静态的,生命周期与类相同。除非类被卸载,否则这个引用将一直存在。这意味着Entry的弱引用永远不会为null,ThreadLocalMap也就不会自动清除这些Entry,导致内存无法释放。
因此,要让ThreadLocal中的弱引用机制发挥作用,必须确保ThreadLocal对象的生命周期与函数调用周期一致。否则,就必须显式调用remove方法来释放内存。
网上关于ThreadLocal内存泄漏的说法是正确的,但往往没有深入解释其原因。只有理解了ThreadLocalMap的内部实现机制,我们才能真正明白这种设计的原因,并合理使用ThreadLocal。