ThreadLocal到底有沒有內存泄漏?
點擊上方藍色“程序猿DD”,選擇“設為星標”
回復“資源”獲取獨家整理的學習資料!

1. 前言
ThreadLocal 也是一個使用頻率較高的類,在框架中也經常見到,比如 Spring。
有關 ThreadLocal 源碼分析的文章不少,其中有個問題常被提及:ThreadLocal 是否存在內存泄漏?
不少文章對此講述比較模糊,經常讓人看完腦子還是一頭霧水,我也有此困惑。因此找時間跟小伙伴討論了一番,總算對這個問題有了一定的理解,這里記錄和分享一下,希望對有同樣困惑的朋友們有所幫助。當然,若有理解不當?shù)牡胤揭矚g迎指正。
啰嗦就到這里,下面先從 ThreadLocal 的一個應用場景開始分析吧。
2. 應用場景
ThreadLocal 的應用場景不少,這里舉個簡單的栗子:單點登錄攔截。
也就是在處理一個 HTTP 請求之前,判斷用戶是否登錄:
若未登錄,跳轉到登錄頁面; 若已登錄,獲取并保存用戶的登錄信息。
先定義一個 UserInfoHolder 類保存用戶的登錄信息,其內部用 ThreadLocal 存儲,示例如下:
public?class?UserInfoHolder?{
????private?static?final?ThreadLocal通過 UserInfoHolder 可以存儲和獲取用戶的登錄信息,以便在業(yè)務中使用。
Spring 項目中,如果我們想在處理一個 HTTP 請求之前或之后做些額外的處理,通常定義一個類繼承 HandlerInterceptorAdapter,然后重寫它的一些方法。舉例如下(僅供參考,省略了一些代碼):
public?class?LoginInterceptor?extends?HandlerInterceptorAdapter?{
????//?...
????
????@Override
????public?boolean?preHandle(HttpServletRequest?request,?HttpServletResponse?response,?Object?handler)
????????????throws?Exception?{
????????
????????//?...
????????//?請求執(zhí)行前,獲取用戶登錄信息并保存
????????Map?userInfoMap?=?getUserInfo();
??????? UserInfoHolder.set(userInfoMap);
????????return?true;
????}
????@Override
????public?void?afterCompletion(HttpServletRequest?request,?HttpServletResponse?response,?Object?handler,?Exception?ex)?{
????????//?請求執(zhí)行后,清理掉用戶信息
????????UserInfoHolder.clear();
????}
}
在本例中,我們在處理一個請求之前獲取用戶的信息,在處理完請求之后,將用戶信息清空。應該有朋友在框架或者自己的項目中見過類似代碼。
下面我們深入 ThreadLocal 的內部,來分析這些方法做了些什么,跟內存泄漏又是怎么扯上關系的。
3. 源碼剖析
3.1 類簽名
先從頭開始,也就是類簽名:
public?class?ThreadLocal<T>?{
}
可見它就是一個普通的類,并沒有實現(xiàn)任何接口、也無父類繼承。
3.2 構造器
ThreadLocal 只有一個無參構造器:
public?ThreadLocal()?{
}
此外,JDK 1.8 引入了一個使用 lambda 表達式初始化的靜態(tài)方法 withInitial,如下:
public?static??ThreadLocal?withInitial(Supplier?extends?S>?supplier)?{
????return?new?SuppliedThreadLocal<>(supplier);
}
該方法也可以初始化一個對象,和構造器也比較接近。
3.3 ThreadLocalMap
3.3.1 主要代碼
ThreadLocalMap 是 ThreadLocal 的一個內部嵌套類。
由于 ThreadLocal 的主要操作實際都是通過 ThreadLocalMap 的方法實現(xiàn)的,因此先分析 ThreadLocalMap 的主要代碼:
public?class?ThreadLocal<T>?{
????//?生成?ThreadLocal?的哈希碼,用于計算在?Entry?數(shù)組中的位置
????private?final?int?threadLocalHashCode?=?nextHashCode();
????private?static?final?int?HASH_INCREMENT?=?0x61c88647;
????private?static?int?nextHashCode()?{
????????return?nextHashCode.getAndAdd(HASH_INCREMENT);
????}
????//?...
????
????static?class?ThreadLocalMap?{
????????static?class?Entry?extends?WeakReference<ThreadLocal>>?{
????????????Object?value;
????????????Entry(ThreadLocal>?k,?Object?v)?{
????????????????super(k);
????????????????value?=?v;
????????????}
????????}
????
????????//?初始容量,必須是?2?的次冪
????????private?static?final?int?INITIAL_CAPACITY?=?16;
????
????????//?存儲數(shù)據(jù)的數(shù)組
????????private?Entry[]?table;
????????//?table?中的?Entry?數(shù)量
????????private?int?size?=?0;
????????//?擴容的閾值
????????private?int?threshold;?//?Default?to?0
????
????????//?設置擴容閾值
????????private?void?setThreshold(int?len)?{
????????????threshold?=?len?*?2?/?3;
????????}????
????
????????//?第一次添加元素使用的構造器
????????ThreadLocalMap(ThreadLocal>?firstKey,?Object?firstValue)?{
????????????table?=?new?Entry[INITIAL_CAPACITY];
????????????int?i?=?firstKey.threadLocalHashCode?&?(INITIAL_CAPACITY?-?1);
????????????table[i]?=?new?Entry(firstKey,?firstValue);
????????????size?=?1;
????????????setThreshold(INITIAL_CAPACITY);
????????}
????????
????????//?...
????}
}
ThreadLocalMap 的內部結構其實跟 HashMap 很類似,可以對比前面「JDK源碼分析-HashMap(1)」對 HashMap 的分析。
二者都是「鍵-值對」構成的數(shù)組,對哈希沖突的處理方式不同,導致了它們在結構上產生了一些區(qū)別:
HashMap 處理哈希沖突使用的「鏈表法」。也就是當產生沖突時拉出一個鏈表,而且 JDK 1.8 進一步引入了紅黑樹進行優(yōu)化。 ThreadLocalMap 則使用了「開放尋址法」中的「線性探測」。即,當某個位置出現(xiàn)沖突時,從當前位置往后查找,直到找到一個空閑位置。
其它部分大體是類似的。
3.3.2 注意事項
弱引用
有個值得注意的地方是:ThreadLocalMap 的 Entry 繼承了 WeakReference 類,也就是弱引用類型。
跟進 Entry 的父類,可以看到 ThreadLocal 最終賦值給了 WeakReference 的父類 Reference 的 referent 屬性。即,可以認為 Entry 持有了兩個對象的引用:ThreadLocal 類型的「弱引用」和 Object 類型的「強引用」,其中 ThreadLocal 為 key,Object 為 value。如圖所示:

ThreadLocal 在某些情況可能產生的「內存泄漏」就跟這個「弱引用」有關,后面再展開分析。
尋址
Entry 的 key 是 ThreadLocal 類型的,它是如何在數(shù)組中散列的呢?
ThreadLocal 有個 threadLocalHashCode 變量,每次創(chuàng)建 ThreadLocal 對象時,這個變量都會增加一個固定的值?HASH_INCREMENT,即 0x61c88647,這個數(shù)字似乎跟黃金分割、斐波那契數(shù)有關,但這不是重點,有興趣的朋友可以去深入研究下,這里我們知道它的目的就行了。與 HashMap 的 hash 算法的目的近似,就是為了散列的更均勻。
下面分析 ThreadLocal 的主要方法實現(xiàn)。
3.4 主要方法
ThreadLocal 主要有三個方法:set、get 和 remove,下面分別介紹。
3.4.1 set 方法
set 方法:新增/更新 Entry
public?void?set(T?value)?{
????//?獲取當前線程
????Thread?t?=?Thread.currentThread();
????//?從?Thread?中獲取?ThreadLocalMap
????ThreadLocalMap?map?=?getMap(t);
????if?(map?!=?null)
????????map.set(this,?value);
????else
????????createMap(t,?value);
}
ThreadLocalMap?getMap(Thread?t)?{
????return?t.threadLocals;
}
threadLocals 是 Thread 持有的一個 ThreadLocalMap 引用,默認是 null:
public?class?Thread?implements?Runnable?{
????//?其他代碼...
????ThreadLocal.ThreadLocalMap?threadLocals?=?null;
}
執(zhí)行流程
若從當前 Thread 拿到的 ThreadLocalMap 為空,表示該屬性并未初始化,執(zhí)行 createMap 初始化:
void?createMap(Thread?t,?T?firstValue)?{
????t.threadLocals?=?new?ThreadLocalMap(this,?firstValue);
}
若已存在,則調用 ThreadLocalMap 的 set 方法:
private?void?set(ThreadLocal>?key,?Object?value)?{????
????Entry[]?tab?=?table;
????int?len?=?tab.length;
????//?1.?計算?key?在數(shù)組中的下標?i
????int?i?=?key.threadLocalHashCode?&?(len-1);
????
????//?1.1?若數(shù)組下標為?i?的位置有元素
????//?判斷 i 位置的 Entry 是否為空;不為空則從 i 開始向后遍歷數(shù)組
????for?(Entry?e?=?tab[i];
?????????e?!=?null;
?????????e?=?tab[i?=?nextIndex(i,?len)])?{
????????ThreadLocal>?k?=?e.get();
????????
????????//?索引為?i?的元素就是要查找的元素,用新值覆蓋舊值,到此返回
????????if?(k?==?key)?{
????????????e.value?=?value;
????????????return;
????????}
????????
????????//?索引為?i?的元素并非要查找的元素,且該位置中?Entry?的?Key?已經是?null
????????//?Key?為?null?表明該?Entry?已經過期了,此時用新值來替換這個位置的過期值
????????if?(k?==?null)?{
????????????//?替換過期的?Entry,
????????????replaceStaleEntry(key,?value,?i);
????????????return;
????????}
????}
????
????//?1.2?若數(shù)組下標為?i?的位置為空,將要存儲的元素放到?i?的位置
????tab[i]?=?new?Entry(key,?value);
????int?sz?=?++size;
????//?若未清理過期的?Entry,且數(shù)組的大小達到閾值,執(zhí)行?rehash?操作
????if?(!cleanSomeSlots(i,?sz)?&&?sz?>=?threshold)
????????rehash();
}
先總結下 set 方法主要流程:
首先根據(jù) key 的 threadLocalHashCode 計算它的數(shù)組下標:
如果數(shù)組下標的 Entry 不為空,表示該位置已經有元素。由于可能存在哈希沖突,因此這個位置的元素可能并不是要找的元素,所以遍歷數(shù)組去比較 如果找到等于當前 key 的 Entry,則用新值替換舊值,返回。 如果遍歷過程中,遇到 Entry 不為空、但是 Entry 的 key 為空的情況,則會做一些清理工作。 如果數(shù)組下標的 Entry 為空,直接將元素放到這里,必要時進行擴容。
replaceStaleEntry:替換過期的值,并清理一些過期的 Entry
private?void?replaceStaleEntry(ThreadLocal>?key,?Object?value,
???????????????????????????????int?staleSlot)?{
????Entry[]?tab?=?table;
????int?len?=?tab.length;
????Entry?e;
????
????//?從?staleSlot?開始向前遍歷,若遇到過期的槽(Entry?的?key?為空),更新?slotToExpunge
????//?直到?Entry?為空停止遍歷
????int?slotToExpunge?=?staleSlot;
????for?(int?i?=?prevIndex(staleSlot,?len);
?????????(e?=?tab[i])?!=?null;
?????????i?=?prevIndex(i,?len))
????????if?(e.get()?==?null)
????????????slotToExpunge?=?i;
????
????//?從?staleSlot?開始向后遍歷,若遇到與當前?key?相等的?Entry,更新舊值,并將二者換位置
????//?目的是把它放到「應該」在的位置
????for?(int?i?=?nextIndex(staleSlot,?len);
?????????(e?=?tab[i])?!=?null;
?????????i?=?nextIndex(i,?len))?{
????????ThreadLocal>?k?=?e.get();
????????
????????if?(k?==?key)?{
????????????//?更新舊值
????????????e.value?=?value;
????????????
????????????//?換位置
????????????tab[i]?=?tab[staleSlot];
????????????tab[staleSlot]?=?e;
????????????
????????????//?Start?expunge?at?preceding?stale?entry?if?it?exists
????????????if?(slotToExpunge?==?staleSlot)
????????????????slotToExpunge?=?i;
????????????cleanSomeSlots(expungeStaleEntry(slotToExpunge),?len);
????????????return;
????????}
????????
????????if?(k?==?null?&&?slotToExpunge?==?staleSlot)
????????????slotToExpunge?=?i;
????}
????
????//?If?key?not?found,?put?new?entry?in?stale?slot
????//?若未找到?key,說明?Entry?此前并不存在,新增
????tab[staleSlot].value?=?null;
????tab[staleSlot]?=?new?Entry(key,?value);
????
????//?If?there?are?any?other?stale?entries?in?run,?expunge?them
????if?(slotToExpunge?!=?staleSlot)
????????cleanSomeSlots(expungeStaleEntry(slotToExpunge),?len);
}
replaceStaleEntry 的主要執(zhí)行流程如下:
從 staleSlot 向前遍歷數(shù)組,直到 Entry 為空時停止遍歷。這一步的主要目的是查找 staleSlot 前面過期的 Entry 的數(shù)組下標 slotToExpunge。 從 staleSlot 向后遍歷數(shù)組 若 Entry 的 key 與給定的 key 相等,將該 Entry 與 staleSlot 下標的 Entry 互換位置。目的是為了讓新增的 Entry 放到它「應該」在的位置。 若找不到相等的 key,說明該 key 對應的 Entry 不在數(shù)組中,將新值放到 staleSlot 位置。該操作其實就是處理哈希沖突的「線性探測」方法:當某個位置已被占用,向后探測下一個位置。 若 staleSlot 前面存在過期的 Entry,則執(zhí)行清理操作。
PS: 所謂 Entry「應該」在的位置,就是根據(jù) key 的 threadLocalHashCode 與數(shù)組長度取余計算出來的位置,即?
k.threadLocalHashCode & (len - 1)?,或者哈希沖突之后的位置,這里只是為了方便描述。
expungeStaleEntry:清理過期的 Entry
//?staleSlot?表示過期的槽位(即?Entry?數(shù)組的下標)
private?int?expungeStaleEntry(int?staleSlot)?{
????Entry[]?tab?=?table;
????int?len?=?tab.length;
????
????//?1.?將給定位置的?Entry?置為?null
????tab[staleSlot].value?=?null;
????tab[staleSlot]?=?null;
????size--;
????
????//?Rehash?until?we?encounter?null
????Entry?e;
????int?i;
????//?遍歷數(shù)組
????for?(i?=?nextIndex(staleSlot,?len);
?????????(e?=?tab[i])?!=?null;
?????????i?=?nextIndex(i,?len))?{
????????//?獲取?Entry?的?key
????????ThreadLocal>?k?=?e.get();
????????if?(k?==?null)?{
????????????//?若?key?為?null,表示?Entry?過期,將?Entry?置空
????????????e.value?=?null;
????????????tab[i]?=?null;
????????????size--;
????????}?else?{
????????????//?key?不為空,表示?Entry?未過期
????????????//?計算?key?的位置,若?Entry?不在它「應該」在的位置,把它移到「應該」在的位置
????????????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;
}
該方法主要做了哪些工作呢?
清空給定位置的 Entry 從給定位置的下一個開始向后遍歷數(shù)組 若遇到 Entry 為 null,結束遍歷 若遇到 key 為空的 Entry(即過期的),就將該 Entry 置空 若遇到 key 不為空的 Entry,而且經過計算,該 Entry 并不在它「應該」在的位置,則將其移動到它「應該」在的位置 返回 staleSlot 后面的、Entry 為 null 的索引下標
cleanSomeSlots:清理一些槽(Slot)
private?boolean?cleanSomeSlots(int?i,?int?n)?{
????boolean?removed?=?false;
????Entry[]?tab?=?table;
????int?len?=?tab.length;
????do?{
????????i?=?nextIndex(i,?len);
????????Entry?e?=?tab[i];
????????//?Entry?不為空、key?為空,即?Entry?過期
????????if?(e?!=?null?&&?e.get()?==?null)?{
????????????n?=?len;
????????????removed?=?true;
????????????//?清理?i?后面連續(xù)過期的?Entry,直到?Entry?為?null,返回該?Entry?的下標
????????????i?=?expungeStaleEntry(i);
????????}
????}?while?(?(n?>>>=?1)?!=?0);
????return?removed;
}
該方法做了什么呢?從給定位置的下一個開始掃描數(shù)組,若遇到 key 為空的 Entry(過期的),則清理該位置及其后面過期的槽。
值得注意的是,該方法循環(huán)執(zhí)行的次數(shù)為 log(n)。由于該方法是在 set 方法內部被調用的,也就是新增/更新時:
如果不掃描和清理,set 方法執(zhí)行速度很快,但是會存在一些垃圾(過期的 Entry); 如果每次都掃描清理,不會存在垃圾,但是插入性能會降低到 O(n)。
因此,這個次數(shù)其實就一種平衡策略:Entry 數(shù)組較小時,就少清理幾次;數(shù)組較大時,就多清理幾次。
rehash:調整 Entry 數(shù)組
private?void?rehash()?{
????//?清理數(shù)組中過期的?Entry
????expungeStaleEntries();
????//?Use?lower?threshold?for?doubling?to?avoid?hysteresis
????if?(size?>=?threshold?-?threshold?/?4)
????????resize();
}
//?從頭開始清理整個?Entry?數(shù)組
private?void?expungeStaleEntries()?{
????Entry[]?tab?=?table;
????int?len?=?tab.length;
????for?(int?j?=?0;?j?????????Entry?e?=?tab[j];
????????if?(e?!=?null?&&?e.get()?==?null)
????????????expungeStaleEntry(j);
????}
}
該方法主要作用:
清理數(shù)組中過期的 Entry 若清理后 Entry 的數(shù)量大于等于 threshold 的 3/4,則執(zhí)行 resize 方法進行擴容
resize 方法:Entry 數(shù)組擴容
/**
?*?Double?the?capacity?of?the?table.
?*/
private?void?resize()?{
????Entry[]?oldTab?=?table;
????int?oldLen?=?oldTab.length;
????int?newLen?=?oldLen?*?2;?//?新長度為舊長度的兩倍
????Entry[]?newTab?=?new?Entry[newLen];
????int?count?=?0;
????
????//?遍歷舊的?Entry?數(shù)組,將數(shù)組中的值移到新數(shù)組中
????for?(int?j?=?0;?j?????????Entry?e?=?oldTab[j];
????????if?(e?!=?null)?{
????????????ThreadLocal>?k?=?e.get();
????????????//?若?Entry?的?key?已過期,則將?Entry?清理掉
????????????if?(k?==?null)?{
????????????????e.value?=?null;?//?Help?the?GC
????????????}?else?{
????????????????//?計算在新數(shù)組中的位置
????????????????int?h?=?k.threadLocalHashCode?&?(newLen?-?1);
????????????????//?哈希沖突,線性探測下一個位置
????????????????while?(newTab[h]?!=?null)
????????????????????h?=?nextIndex(h,?newLen);
????????????????newTab[h]?=?e;
????????????????count++;
????????????}
????????}
????}
????
????//?設置新的閾值
????setThreshold(newLen);
????size?=?count;
????table?=?newTab;
}
該方法的作用是 Entry 數(shù)組擴容,主要流程:
創(chuàng)建一個新數(shù)組,長度為原數(shù)組的 2 倍; 從下標 0 開始遍歷舊數(shù)組的所有元素 若元素已過期(key 為空),則將 value 也置空 將未過期的元素移到新數(shù)組
3.4.2 get 方法
分析完了 set 方法,再看 get 方法就相對容易了不少。
get 方法:獲取 ThreadLocal 對應的 Entry
public?T?get()?{
????Thread?t?=?Thread.currentThread();
????ThreadLocalMap?map?=?getMap(t);
????if?(map?!=?null)?{
????????ThreadLocalMap.Entry?e?=?map.getEntry(this);
????????if?(e?!=?null)?{
????????????@SuppressWarnings("unchecked")
????????????T?result?=?(T)e.value;
????????????return?result;
????????}
????}
????return?setInitialValue();
}
get 方法首先獲取當前線程的 ThreadLocalMap 并判斷:
若 Map 已存在,從 Map 中取值 若 Map 不存在,或者 Map 中獲取的值為空,執(zhí)行 setInitialValue 方法
setInitialValue 方法:獲取/設置初始值
private?T?setInitialValue()?{
????//?獲取初始值
????T?value?=?initialValue();
????Thread?t?=?Thread.currentThread();
????ThreadLocalMap?map?=?getMap(t);
????if?(map?!=?null)
????????map.set(this,?value);
????else
????????createMap(t,?value);
????return?value;
}
protected?T?initialValue()?{
????return?null;
}
先取初始值,這個初始值默認為空(該方法是 protected,可以由子類初始化)。
若 Thread 的 ThreadLocalMap 已初始化,則將初始值存入 Map 否則,創(chuàng)建 ThreadLocalMap 返回初始值
除了初始值,其他邏輯跟 set 方法是一樣的,這里不再贅述。
PS: 可以看到初始值是惰性初始化的。
getEntry:從 Entry 數(shù)組中獲取給定 key 對應的 Entry
private?Entry?getEntry(ThreadLocal>?key)?{
????//?計算下標
????int?i?=?key.threadLocalHashCode?&?(table.length?-?1);
????Entry?e?=?table[i];
????//?查找命中
????if?(e?!=?null?&&?e.get()?==?key)
????????return?e;
????else
????????return?getEntryAfterMiss(key,?i,?e);
}
//?key?未命中
private?Entry?getEntryAfterMiss(ThreadLocal>?key,?int?i,?Entry?e)?{
????Entry[]?tab?=?table;
????int?len?=?tab.length;
????
????//?遍歷數(shù)組
????while?(e?!=?null)?{
????????ThreadLocal>?k?=?e.get();
????????if?(k?==?key)
????????????return?e;?//?是要找的?key,返回
????????if?(k?==?null)
????????????expungeStaleEntry(i);?//?Entry?已過期,清理?Entry
????????else
????????????i?=?nextIndex(i,?len);?//?向后遍歷
????????e?=?tab[i];
????}
????return?null;
}
3.4.3 remove 方法
remove 方法:移除 ThreadLocal 對應的 Entry
public?void?remove()?{
????ThreadLocalMap?m?=?getMap(Thread.currentThread());
????if?(m?!=?null)
????????m.remove(this);
}
這里調用了 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();
????????????expungeStaleEntry(i);
????????????return;
????????}
????}
}
其中 e.clear 調用的是 Entry 的父類 Reference 的 clear 方法:
public?void?clear()?{
????this.referent?=?null;
}
其實就是將 Entry 的 key 置空。
remove 方法的主要執(zhí)行流程如下:
獲取當前線程的 ThreadLocalMap 以當前 ThreadLocal 做為 key,從 Map 中查找相應的 Entry,將 Entry 的 key 置空 將該 ThreadLocal 對應的 Entry 置空,并向后遍歷清理 Entry 數(shù)組,也就是 expungeStaleEntry 方法的操作,前面已經分析過了,這里不再贅述。
3.4.4 主要方法小結
ThreadLocal 的主要方法 set、get 和 remove 前面已經分析過,這里簡單做個小結。
set 方法
以當前 ThreadLocal 為 key、新增的 Object 為 value 組成一個 Entry,放入 ThreadLocalMap,也就是 Entry 數(shù)組中。 計算 Entry 的位置后 若該槽為空,直接放到這里;并清理一些過期的 Entry,必要時進行擴容。 當遇到散列沖突時,線性探測向后查找數(shù)組中為空的、或者已經過期的槽,用新值替換。
get 方法
以當前 ThreadLocal 為 key,從 Entry 數(shù)組中查找對應 Entry 的 value 若 ThreadLocalMap 未初始化,則用給定初始值將其初始化 若 ThreadLocalMap 已初始化,從 Entry 數(shù)組查找 key
remove 方法:以當前 ThreadLocal 為 key,從 Entry 數(shù)組清理掉對應的 Entry,并且再清理該位置后面的、過期的 Entry
方法雖少,但是稍微有點繞,除了做本身的功能,都執(zhí)行了一些額外的清理操作。
分析了這幾個方法的源碼之后,下面就來研究一下內存泄漏的問題。
4. 內存泄漏分析
首先說明一點,ThreadLocal 通常作為成員變量或靜態(tài)變量來使用(也就是共享的),比如前面應用場景中的例子。因為局部變量已經在同一條線程內部了,沒必要使用 ThreadLocal。
為便于理解,這里先給出了 Thread、ThreadLocal、ThreadLocalMap、Entry 這幾個類在 JVM 的內存示意圖:

簡單說明:
當一個線程運行時,棧中存在當前 Thread 的棧幀,它持有 ThreadLocalMap 的強引用。
ThreadLocal 所在的類持有一個 ThreadLocal 的強引用;同時,ThreadLocalMap 中的 Entry 持有一個 ThreadLocal 的弱引用。
4.1 場景一
若方法執(zhí)行完畢、線程正常消亡,則 Thread 的 ThreadLocalMap 引用將斷開,如圖:

以后 GC 發(fā)生時,弱引用也會斷開,整個 ThreadLocalMap 都會被回收掉,不存在內存泄漏。
4.2 場景二
如果是線程池中的線程呢?也就是線程一直存活。經過 GC 后 Entry 持有的 ThreadLocal 引用斷開,Entry 的 key 為空,value 不為空,如圖所示:

此時,如果沒有任何 remove 或者 get 等清理 Entry 數(shù)組的動作,那么該 Entry 的 value 持有的 Object 就不會被回收掉。這樣就產生了內存泄漏。
這種情況其實也很容易避免,使用完執(zhí)行 remove 方法就行了。
5. 小結
本文分析了 ThreadLocal 的主要方法實現(xiàn),并分析了它可能存在內存泄漏的場景。
ThreadLocal 主要用于當前線程從共享變量中保存一份「副本」,常用的一個場景就是單點登錄保存用戶的登錄信息。 ThreadLocal 將數(shù)據(jù)存儲在 ThreadLocalMap 中,ThreadLocalMap 是由 Entry 構成的數(shù)組,結構有點類似 HashMap。 ThreadLocal 使用不當可能會造成內存泄漏。避免內存泄漏的方法是在方法調用結束前執(zhí)行 ThreadLocal 的 remove 方法。
往期推薦
掃一掃,關注我
一起學習,一起進步
每周贈書,福利不斷
﹀
﹀
﹀
深度內容
推薦加入
最近熱門分享話題:?#滬牌代拍的技術、策略與設計
