看很多資料說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已清除
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIn5GcuAzM5QzNxcTM0ATMxAjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
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。