java8 ThreadLocal真不会内存泄露了吗

看很多资料说Java8中ThreadLocal使用了虚引用以及set、get、remove会清理ThreadLocalMap中key为null的数据,这样就不会有内存泄露问题。真的是这样吗?如果是真的,key怎么为null的?怎么清理的?想找到答案,还是从源码入手。

一、set,直接定位到ThreadLocalMap.set

1):
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); -- 获取hash对应槽位

for (Entry e = tab[i];
     e != null; 
     e = tab[i = nextIndex(i, len)]) {  -- 如果e不为null就获取下一个槽位,如果i=len-1则会再从0开始
    ThreadLocal<?> k = e.get();         -- 获取当前槽位的key值

    if (k == key) {                     -- 如果key值相同则直接替换
        e.value = value;
        return;
    }

    if (k == null) {                    -- key == null说明当前线程对ThreadLocal已无关联,但Entry还存在当前槽位中
        replaceStaleEntry(key, value, i); -- 清空部分槽位后加入,下面讲
        return;                           -- 满足条件直接返回
    }
}

tab[i] = new Entry(key, value);          -- 如果没有hash冲突或找到下一个空槽位则直接添加
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold) -- 清空部分槽位并判断size是否到阀值
    rehash();                                  -- 清空key为null的槽位,并增大数组长度

主要流程为:1、获取key的hash对应槽位

                      2、判断当前槽位是否已被占用,若已被占用,则判断key值是否相同,是则直接替换;否则则判断槽位中的

                            Entry的key值是否为null,为null则走replaceStaleEntry方法;

                             key不相同或不为null则一直往下找,直到遇到空槽位或key相同或key值为null的槽位

                     3、若直接找到为空的槽位,则放入并清空部分槽位并判断是否需要扩容

2):下面看replaceStaleEntry(key, value, i)逻辑,即添加数据时发生冲突并且冲突数据的key值为null

 

Entry[] tab = table;
int len = tab.length;
Entry e;

//清理数据的入口
int slotToExpunge = staleSlot; -- 传入的i,即key == null的槽位
for (int i = prevIndex(staleSlot, len);  -- 从i位往前遍历数组,i=0时跳到len-1
     (e = tab[i]) != null;               -- 直到空槽位
     i = prevIndex(i, len)) 
    if (e.get() == null)
        slotToExpunge = i;               -- 空槽位后第一个key为null的槽
for (int i = nextIndex(staleSlot, len);  -- 从i往后遍历
     (e = tab[i]) != null;               -- 直到空槽位
     i = nextIndex(i, len)) {
    ThreadLocal<?> k = e.get();
    if (k == key) {                     -- 如果找到key与传入的key相同,则替换, 并将新值替换到staleSlot位
        e.value = value;

        tab[i] = tab[staleSlot];
        tab[staleSlot] = e;

        if (slotToExpunge == staleSlot)  -- 如果staleSlot之前没有要清除的值,则从i位置开始
            slotToExpunge = i;
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); -- 清除数据
        return;
    }

    if (k == null && slotToExpunge == staleSlot)  -- 若果后面有需要清理的数据,但是前面没有,则设置清理点为i
        slotToExpunge = i;
}
tab[staleSlot].value = null;               
tab[staleSlot] = new Entry(key, value);

if (slotToExpunge != staleSlot)          -- 如果还有需要清理的数据,则走清理逻辑
    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);

说明:1、找到第一个要清理的槽位位置slotToExpunge,如果有两个及以上的key为null的entry,则调用cleanSomeSlots

           2、将要添加的值放到staleSlot位置

3):expungeStaleEntry逻辑,主要是value=null,释放value和entry,让垃圾收集器回收

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

    
    tab[staleSlot].value = null; -- 释放value
    tab[staleSlot] = null;       -- 释放entry
    size--;

    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len); -- 从i往后遍历直到null
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {
            e.value = null;  -- 释放value
            tab[i] = null;   -- 释放entry
            size--;
        } else {                        -- 若key不为null
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {               -- 如果之前k出现过hash冲突,则将k放入冲突槽位后第一个为null的槽位
                tab[i] = null;
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;                           --返回为staleSlot后第一个为空的槽位
}

说明:从staleSlot即清理点开始清除key为null的数据并返回下一个空槽位

1)、2)、3)代码可清除插入点前为空的槽位到后为空的槽位之间的key为null的数据,即两个空之间需要清除的entry已清除

4):cleanSomeSlots逻辑

 

private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        i = nextIndex(i, len);         -- 从i往后遍历
        Entry e = tab[i];
        if (e != null && e.get() == null) { -- 遇到需要清除的key
            n = len;                   -- 重置n
            removed = true;
            i = expungeStaleEntry(i);   -- 清除i到第一个空槽位之间需要清除的key
        }
    } while ( (n >>>= 1) != 0);        -- 每次循环n减半,即若2^x=len,则循环x+1次
    return removed;
}

说明:从i位置遍历x+1个数据,若遇到需清理数据,则清除i到第一个空槽位之间需要清除的数据并重置n,否则退出。由于不是全遍历,所以还会有key=null的entry没有清除

总结:

set逻辑:1、若没有hash冲突,则直接插入,并调用cleanSomeSlots清除部分key为null的entry,若没有数据清除并且达到

                     扩容阀值,则进行扩容(扩容时会全遍历删除key=null的数据)

                 2、若有hash冲突:1):key相同则直接替换value;

                                                 2):若存在entry但是key为null,则将要添加的值插入, 并清理插入点前后两个空槽之间

                                                           key为null的数据,若两个空槽之间存在两个及以上个key为null的entry,则调用                                                                                  cleanSomeSlots清除部分key为null的entry;

                                                   3):若没有key相同或entry.key为null的情况,则插入遍历到的第一个空槽,

                                                            并调用cleanSomeSlots清除部分key为null的entry,若没有数据清除并且达到

                                                             扩容阀值, 则进行扩容(扩容时会全遍历删除key=null的数据)

二、get,直接定位到ThreadLocalMap.getEntry,直接上源码

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;                   -- 没有冲突时直接返回,没有清entry
    else
        return getEntryAfterMiss(key, i, e);
}
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);           -- 同上,清理i两边空槽之间的key为null的值,
                                                如果有两个及以上的key为null的entry,则调用cleanSomeSlots
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

说明:好像也不能全部清理掉

三、remove,直接定位到ThreadLocalMap.remove,直接上源码

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();               -- 找到key并将entry.key设为null
            expungeStaleEntry(i);   -- 同上,清理i两边空槽之间的key为null的值,
                                        如果有两个及以上的key为null的entry,则调用cleanSomeSlots
            return;
        }
    }
}

说明:不能保证全部清除,但会清除当前key

 

总结:除了map扩容时会遍历整个数组进行清除外,其他方法都不能保证全部清除掉所有key为null的entry,除非线程本身被垃圾收集器回收,但现在用的最多的还是线程池,虽然大部分entry和value会被清理,但还会有部分一直存在内存中,所以也不能杜绝内存泄露,最好还是用完后手动remove为好。

注:目前版本key=null的entry是由于ThreadLocal在entry中是虚引用,在没有强引用时,会被垃圾垃圾收集器回收,回收掉后,entry中对应的key会为null。

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值