天天看點

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已清除

java8 ThreadLocal真不會記憶體洩露了嗎

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。