ThreadLocal内存泄漏问题和解决方法

一、问题分析

测试用例

	Thread thread = Thread.currentThread();
    ThreadLocal<Integer> tl = new ThreadLocal<>();
    tl.set(1);
    System.gc(); // 手动提醒下gc
    Thread.sleep(100); // 让gc先执行
    System.out.println(111);
    tl = null; // 断开ThreadLocal引用
    System.gc();
    Thread.sleep(100);
    System.out.println(111); // 断点打这里

在这里插入图片描述
可以看到线程中的threadLocals还持有这个ThreadLocalMap的引用,其中Entry中value还是有值的为1,但是referent为null,表示已经被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;
    }
}

public abstract class Reference<T> {

	private T referent;         /* Treated specially by GC */

	Reference(T referent) {
        this(referent, null);
    }

    Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }
}

简单来说:ThreadLocalMap还持有Entry对象的引用,所以Entry对象并不会被释放。而由于key是WeakReference中的referent,对于Entry->referent这个关联引用会在gc的时候断开,所以可以看到上面的referent为null,但是ThreadLocalMap中还存在entry对象。所以这样value是会一直被引用的,可能会导致内存溢出的问题存在。

二、如何避免

我们可以使用 remove来避免这种情况

	Thread thread = Thread.currentThread();
    ThreadLocal<Integer> tl = new ThreadLocal<>();
    tl.set(1);
    System.gc(); // 手动提醒下gc
    Thread.sleep(100); // 让gc先执行
    System.out.println(111);
    tl.remove(); // 调用remove
    tl = null; // 断开ThreadLocal引用
    System.gc();
    Thread.sleep(100);
    System.out.println(111); // 断点打这里

在这里插入图片描述
这里table里面只有3个了,说明已经被回收了。

ThreadLocal.remove()方法其实就是获取当前线程的threadLocals(ThreadLocalMap)并调用其remove()方法
ps: 可以对照set方法看看实现,这里解决hash冲突的话不是和HashMap一样用拉链法,而是去找下一个没有设置值得结点

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) { // 找到对应的ThreadLocal
            e.clear(); // this.referent = null; 断开引用
            expungeStaleEntry(i); 
            return;
        }
    }
}

继续看 expungeStaleEntry 的实现

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // 将Entry的value也就是ThrealLocal对应的值置为null
    // 断开tab是对应槽位和Entry的关联
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // 下面是rehash操作不是本次重点
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        // ......
    }
    return i;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值