阿粉昨天說我動不動就內(nèi)存泄漏,我好委屈...
大家好,我是 ThreadLocal ,昨天阿粉說我動不動就內(nèi)存泄漏,我蠻委屈的,我才沒有冤枉他嘞,證據(jù)在這里: ThreadLocal 你怎么動不動就內(nèi)存泄漏?
因為人家明明也考慮到了很多情況,做了很多事情,保證了如果沒有 remove ,也有對 key 值為 null 時進行回收的處理操作
啥?你竟然不信?我 ThreadLocal 難道會騙你么
今天為了證明一下自己,我打算從組成的源碼開始講起,在 get , set 方法中都有對 key 值為 null 時進行回收的處理操作,先來看 set 方法是怎么做的
set
下面是 set 方法的源碼:
- private void set(ThreadLocal<?> key, Object value) {
- // We don't use a fast path as with get() because it is at
- // least as common to use set() to create new entries as
- // it is to replace existing ones, in which case, a fast
- // path would fail more often than not.
- Entry[] tab = table;
- int len = tab.length;
- int i = key.threadLocalHashCode & (len-1);
- for (Entry e = tab[i];
- // 如果 e 不為空,說明 hash 沖突,需要向后查找
- e != null;
- // 從這里可以看出, ThreadLocalMap 采用的是開放地址法解決的 hash 沖突
- // 是最經(jīng)典的 線性探測法 --> 我覺得之所以選擇這種方法解決沖突時因為數(shù)據(jù)量不大
- e = tab[i = nextIndex(i, len)]) {
- ThreadLocal<?> k = e.get();
- // 要查找的 ThreadLocal 對象找到了,直接設置需要設置的值,然后 return
- if (k == key) {
- e.value = value;
- return;
- }
- // 如果 k 為 null ,說明有 value 沒有及時回收,此時通過 replaceStaleEntry 進行處理
- // replaceStaleEntry 具體內(nèi)容等下分析
- if (k == null) {
- replaceStaleEntry(key, value, i);
- return;
- }
- }
- // 如果 tab[i] == null ,則直接創(chuàng)建新的 entry 即可
- tab[i] = new Entry(key, value);
- int sz = ++size;
- // 在創(chuàng)建之后調(diào)用 cleanSomeSlots 方法檢查是否有 value 值沒有及時回收
- // 如果 sz >= threshold ,則需要擴容,重新 hash 即, rehash();
- if (!cleanSomeSlots(i, sz) && sz >= threshold)
- rehash();
- }
通過源碼可以看到,在 set 方法中,主要是通過 replaceStaleEntry 方法和 cleanSomeSlots 方法去做的檢測和處理
接下來瞅瞅 replaceStaleEntry 都干了點兒啥
replaceStaleEntry
- private void replaceStaleEntry(ThreadLocal<?> key, Object value,
- int staleSlot) {
- Entry[] tab = table;
- int len = tab.length;
- Entry e;
- // 從當前 staleSlot 位置開始向前遍歷
- int slotToExpunge = staleSlot;
- for (int i = prevIndex(staleSlot, len);
- (e = tab[i]) != null;
- i = prevIndex(i, len))
- if (e.get() == null)
- // 當 e.get() == null 時, slotToExpunge 記錄下此時的 i 值
- // 即 slotToExpunge 記錄的是 staleSlot 左手邊第一個空的 Entry
- slotToExpunge = i;
- // 接下來從當前 staleSlot 位置向后遍歷
- // 這兩個遍歷是為了清理在左邊遇到的第一個空的 entry 到右邊的第一個空的 entry 之間所有過期的對象
- // 但是如果在向后遍歷過程中,找到了需要設置值的 key ,就開始清理,不會再繼續(xù)向下遍歷
- for (int i = nextIndex(staleSlot, len);
- (e = tab[i]) != null;
- i = nextIndex(i, len)) {
- ThreadLocal<?> k = e.get();
- // 如果 k == key 說明在插入之前就已經(jīng)有相同的 key 值存在,所以需要替換舊的值
- // 同時和前面過期的對象進行交換位置
- if (k == key) {
- e.value = value;
- tab[i] = tab[staleSlot];
- tab[staleSlot] = e;
- // 如果 slotToExpunge == staleSlot 說明向前遍歷時沒有找到過期的
- if (slotToExpunge == staleSlot)
- slotToExpunge = i;
- // 進行清理過期數(shù)據(jù)
- cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
- return;
- }
- // 如果在向后遍歷時,沒有找到 value 被回收的 Entry 對象
- // 且剛開始 staleSlot 的 key 為空,那么它本身就是需要設置 value 的 Entry 對象
- // 此時不涉及到清理
- if (k == null && slotToExpunge == staleSlot)
- slotToExpunge = i;
- }
- // 如果 key 在數(shù)組中找不到,那就好說了,直接創(chuàng)建一個新的就可以了
- tab[staleSlot].value = null;
- tab[staleSlot] = new Entry(key, value);
- // 如果 slotToExpunge != staleSlot 說明存在過期的對象,就需要進行清理
- if (slotToExpunge != staleSlot)
- cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
- }
在 replaceStaleEntry 方法中,需要注意一下剛開始的兩個 for 循環(huán)中內(nèi)容(在這里再貼一下):
- if (e.get() == null)
- // 當 e.get() == null 時, slotToExpunge 記錄下此時的 i 值
- // 即 slotToExpunge 記錄的是 staleSlot 左手邊第一個空的 Entry
- slotToExpunge = i;
- if (k == key) {
- e.value = value;
- tab[i] = tab[staleSlot];
- tab[staleSlot] = e;
- // 如果 slotToExpunge == staleSlot 說明向前遍歷時沒有找到過期的
- if (slotToExpunge == staleSlot)
- slotToExpunge = i;
- // 進行清理過期數(shù)據(jù)
- cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
- return;
- }
這兩個 for 循環(huán)中的 if 到底是在做什么?
看第一個 if ,當 e.get() == null 時,此時將 i 的值給 slotToExpunge
第二個 if ,當 k ==key 時,此時將 i 給了 staleSlot 來進行交換
為什么要對 staleSlot 進行交換呢?畫圖說明一下
如下圖,假設此時表長為 10 ,其中下標為 3 和 5 的 key 已經(jīng)被回收( key 被回收掉的就是 null ),因為采用的開放地址法,所以 15 mod 10 應該是 5 ,但是因為位置被占,所以在 6 的位置,同樣 25 mod 10 也應該是 5 ,但是因為位置被占,下個位置也被占,所以就在第 7 號的位置上了
按照上面的分析,此時 slotToExpunge 值為 3 , staleSlot 值為 5 , i 為 6
假設,假設這個時候如果不進行交換,而是直接回收的話,此時位置為 5 的數(shù)據(jù)就被回收掉,然后接下來要插入一個 key 為 15 的數(shù)據(jù),此時 15 mod 10 算出來是 5 ,正好這個時候位置為 5 的被回收完畢,這個位置就被空出來了,那么此時就會這樣:
同樣的 key 值竟然出現(xiàn)了兩次?!
這肯定是不希望看到的結(jié)果,所以一定要進行數(shù)據(jù)交換
在上面代碼中有一行代碼 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); ,說明接下來的處理是交給了 expungeStaleEntry ,接下來去分析一下 expungeStaleEntry
expungeStaleEntry
- private int expungeStaleEntry(int staleSlot) {
- Entry[] tab = table;
- int len = tab.length;
- // expunge entry at staleSlot
- tab[staleSlot].value = null;
- tab[staleSlot] = null;
- size--;
- // Rehash until we encounter null
- Entry e;
- int i;
- for (i = nextIndex(staleSlot, len);
- (e = tab[i]) != null;
- i = nextIndex(i, len)) {
- ThreadLocal<?> k = e.get();
- // 如果 k == null ,說明 value 就應該被回收掉
- if (k == null) {
- // 此時直接將 e.value 置為 null
- // 這樣就將 thread -> threadLocalMap -> value 這條引用鏈給打破
- // 方便了 GC
- e.value = null;
- tab[i] = null;
- size--;
- } else {
- // 這個時候要重新 hash ,因為采用的是開放地址法,所以可以理解為就是將后面的元素向前移動
- int h = k.threadLocalHashCode & (len - 1);
- if (h != i) {
- tab[i] = null;
- // Unlike Knuth 6.4 Algorithm R, we must scan until
- // null because multiple entries could have been stale.
- while (tab[h] != null)
- h = nextIndex(h, len);
- tab[h] = e;
- }
- }
- }
- return i;
- }
因為是在 replaceStaleEntry 方法中調(diào)用的此方法,傳進來的值是 staleSlot ,繼續(xù)上圖,經(jīng)過 replaceStaleEntry 之后,它的數(shù)據(jù)結(jié)構(gòu)是這樣:
此時傳進來的 staleSlot 值為 6 ,因為此時的 key 為 null ,所以接下來會走 e.value = null ,這一步結(jié)束之后,就成了:
接下來 i 為 7 ,此時的 key 不為 null ,那么就會重新 hash : int h = k.threadLocalHashCode & (len - 1); ,得到的 h 應該是 5 ,但是實際上 i 為 7 ,說明出現(xiàn)了 hash 沖突,就會繼續(xù)向下走,最終的結(jié)果是這樣:
可以看到,原來的 key 為 null ,值為 V5 的已經(jīng)被回收掉了。我認為之所以回收掉之后,還要再次進行重新 hash ,就是為了防止 key 值重復插入情況的發(fā)生
假設 key 為 25 的并沒有進行向前移動,也就是它還在位置 7 ,位置 6 是空的,再插入一個 key 為 25 ,經(jīng)過 hash 應該在位置 5 ,但是有數(shù)據(jù)了,那就向下走,到了位置 6 ,誒,竟然是空的,趕緊插進去,這不就又造成了上面說到的問題,同樣的一個 key 竟然出現(xiàn)了兩次?!
而且經(jīng)過 expungeStaleEntry 之后,將 key 為 null 的值,也設置為了 null ,這樣就方便 GC
分析到這里應該就比較明確了,在 expungeStaleEntry 中,有些地方是幫助 GC 的,而通過源碼能夠發(fā)現(xiàn), set 方法調(diào)用了該方法進行了 GC 處理, get 方法也有,不信你瞅瞅:
get
- private Entry getEntry(ThreadLocal<?> key) {
- int i = key.threadLocalHashCode & (table.length - 1);
- Entry e = table[i];
- // 如果能夠找到尋找的值,直接 return 即可
- if (e != null && e.get() == key)
- return e;
- else
- // 如果找不到,則調(diào)用 getEntryAfterMiss 方法去處理
- 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)
- // 如果 k == null 說明有 value 沒有及時回收
- // 調(diào)用 expungeStaleEntry 方法去處理,幫助 GC
- expungeStaleEntry(i);
- else
- i = nextIndex(i, len);
- e = tab[i];
- }
- return null;
- }
get 和 set 方法都有進行幫助 GC ,所以正常情況下是不會有內(nèi)存溢出的,但是如果創(chuàng)建了之后一直沒有調(diào)用 get 或者 set 方法,還是有可能會內(nèi)存溢出
所以最保險的方法就是,使用完之后就及時 remove 一下,加快垃圾回收,就完美的避免了垃圾回收
我 ThreadLocal 雖然沒辦法做到 100% 的解決內(nèi)存泄漏問題,但是我能做到 80% 不也應該夸夸我嘛