日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

ThreadLocal到底有没有内存泄漏?从源码角度来剖析一波

發布時間:2025/3/16 编程问答 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 ThreadLocal到底有没有内存泄漏?从源码角度来剖析一波 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

1. 前言

ThreadLocal 也是一個使用頻率較高的類,在框架中也經常見到,比如 Spring。

有關 ThreadLocal 源碼分析的文章不少,其中有個問題常被提及:ThreadLocal 是否存在內存泄漏?

不少文章對此講述比較模糊,經常讓人看完腦子還是一頭霧水,我也有此困惑。因此找時間跟小伙伴討論了一番,總算對這個問題有了一定的理解,這里記錄和分享一下,希望對有同樣困惑的朋友們有所幫助。當然,若有理解不當的地方也歡迎指正。

啰嗦就到這里,下面先從 ThreadLocal 的一個應用場景開始分析吧。


2. 應用場景

ThreadLocal 的應用場景不少,這里舉個簡單的栗子:單點登錄攔截。

也就是在處理一個 HTTP 請求之前,判斷用戶是否登錄:

  • 若未登錄,跳轉到登錄頁面;

  • 若已登錄,獲取并保存用戶的登錄信息。

先定義一個 UserInfoHolder 類保存用戶的登錄信息,其內部用 ThreadLocal 存儲,示例如下:

public?class?UserInfoHolder?{private?static?final?ThreadLocal<Map<String,?String>>?USER_INFO_THREAD_LOCAL?=?new?ThreadLocal<>();public?static?void?set(Map<String,?String>?map)?{USER_INFO_THREAD_LOCAL.set(map);}public?static?Map<String,?String>?get()?{return?USER_INFO_THREAD_LOCAL.get();}public?static?void?clear()?{USER_INFO_THREAD_LOCAL.remove();}//?... }

通過 UserInfoHolder 可以存儲和獲取用戶的登錄信息,以便在業務中使用。

Spring 項目中,如果我們想在處理一個 HTTP 請求之前或之后做些額外的處理,通常定義一個類繼承 HandlerInterceptorAdapter,然后重寫它的一些方法。舉例如下(僅供參考,省略了一些代碼):

public?class?LoginInterceptor?extends?HandlerInterceptorAdapter?{//?...@Overridepublic?boolean?preHandle(HttpServletRequest?request,?HttpServletResponse?response,?Object?handler)throws?Exception?{//?...//?請求執行前,獲取用戶登錄信息并保存Map<String,?String>?userInfoMap?=?getUserInfo();UserInfoHolder.set(userInfoMap);return?true;}@Overridepublic?void?afterCompletion(HttpServletRequest?request,?HttpServletResponse?response,?Object?handler,?Exception?ex)?{//?請求執行后,清理掉用戶信息UserInfoHolder.clear();} }

在本例中,我們在處理一個請求之前獲取用戶的信息,在處理完請求之后,將用戶信息清空。應該有朋友在框架或者自己的項目中見過類似代碼。

下面我們深入 ThreadLocal 的內部,來分析這些方法做了些什么,跟內存泄漏又是怎么扯上關系的。


3. 源碼剖析

3.1 類簽名

先從頭開始,也就是類簽名:

public?class?ThreadLocal<T>?{ }

可見它就是一個普通的類,并沒有實現任何接口、也無父類繼承。

3.2 構造器

ThreadLocal 只有一個無參構造器:

public?ThreadLocal()?{ }

此外,JDK 1.8 引入了一個使用 lambda 表達式初始化的靜態方法 withInitial,如下:

public?static?<S>?ThreadLocal<S>?withInitial(Supplier<??extends?S>?supplier)?{return?new?SuppliedThreadLocal<>(supplier); }

該方法也可以初始化一個對象,和構造器也比較接近。

3.3 ThreadLocalMap

3.3.1 主要代碼

ThreadLocalMap 是 ThreadLocal 的一個內部嵌套類。

由于 ThreadLocal 的主要操作實際都是通過 ThreadLocalMap 的方法實現的,因此先分析 ThreadLocalMap 的主要代碼:

public?class?ThreadLocal<T>?{//?生成?ThreadLocal?的哈希碼,用于計算在?Entry?數組中的位置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;//?存儲數據的數組private?Entry[]?table;//?table?中的?Entry?數量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 的分析。

二者都是「鍵-值對」構成的數組,對哈希沖突的處理方式不同,導致了它們在結構上產生了一些區別:

  • HashMap 處理哈希沖突使用的「鏈表法」。也就是當產生沖突時拉出一個鏈表,而且 JDK 1.8 進一步引入了紅黑樹進行優化。

  • ThreadLocalMap 則使用了「開放尋址法」中的「線性探測」。即,當某個位置出現沖突時,從當前位置往后查找,直到找到一個空閑位置。

  • 其它部分大體是類似的。

    3.3.2 注意事項

    • 弱引用

    有個值得注意的地方是:ThreadLocalMap 的 Entry 繼承了 WeakReference 類,也就是弱引用類型。

    跟進 Entry 的父類,可以看到 ThreadLocal 最終賦值給了 WeakReference 的父類 Reference 的 referent 屬性。即,可以認為 Entry 持有了兩個對象的引用:ThreadLocal 類型的「弱引用」和 Object 類型的「強引用」,其中 ThreadLocal 為 key,Object 為 value。如圖所示:

    ThreadLocal 在某些情況可能產生的「內存泄漏」就跟這個「弱引用」有關,后面再展開分析。

    • 尋址

    Entry 的 key 是 ThreadLocal 類型的,它是如何在數組中散列的呢?

    ThreadLocal 有個 threadLocalHashCode 變量,每次創建 ThreadLocal 對象時,這個變量都會增加一個固定的值 HASH_INCREMENT,即 0x61c88647,這個數字似乎跟黃金分割、斐波那契數有關,但這不是重點,有興趣的朋友可以去深入研究下,這里我們知道它的目的就行了。與 HashMap 的 hash 算法的目的近似,就是為了散列的更均勻。

    下面分析 ThreadLocal 的主要方法實現。

    3.4 主要方法

    ThreadLocal 主要有三個方法:set、get 和 remove,下面分別介紹。

    3.4.1 set 方法

    • set 方法:新增/更新 Entry

    public?void?set(T?value)?{//?獲取當前線程Thread?t?=?Thread.currentThread();//?從?Thread?中獲取?ThreadLocalMapThreadLocalMap?map?=?getMap(t);if?(map?!=?null)map.set(this,?value);elsecreateMap(t,?value); }ThreadLocalMap?getMap(Thread?t)?{return?t.threadLocals; }

    threadLocals 是 Thread 持有的一個 ThreadLocalMap 引用,默認是 null:

    public?class?Thread?implements?Runnable?{//?其他代碼...ThreadLocal.ThreadLocalMap?threadLocals?=?null; }
    • 執行流程

    若從當前 Thread 拿到的 ThreadLocalMap 為空,表示該屬性并未初始化,執行 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?在數組中的下標?iint?i?=?key.threadLocalHashCode?&?(len-1);//?1.1?若數組下標為?i?的位置有元素//?判斷 i 位置的 Entry 是否為空;不為空則從 i 開始向后遍歷數組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?若數組下標為?i?的位置為空,將要存儲的元素放到?i?的位置tab[i]?=?new?Entry(key,?value);int?sz?=?++size;//?若未清理過期的?Entry,且數組的大小達到閾值,執行?rehash?操作if?(!cleanSomeSlots(i,?sz)?&&?sz?>=?threshold)rehash(); }

    先總結下 set 方法主要流程:

    首先根據 key 的 threadLocalHashCode 計算它的數組下標:

  • 如果數組下標的 Entry 不為空,表示該位置已經有元素。由于可能存在哈希沖突,因此這個位置的元素可能并不是要找的元素,所以遍歷數組去比較

  • 如果找到等于當前 key 的 Entry,則用新值替換舊值,返回。

  • 如果遍歷過程中,遇到 Entry 不為空、但是 Entry 的 key 為空的情況,則會做一些清理工作。

  • 如果數組下標的 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?existsif?(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?themif?(slotToExpunge?!=?staleSlot)cleanSomeSlots(expungeStaleEntry(slotToExpunge),?len); }

    replaceStaleEntry 的主要執行流程如下:

  • 從 staleSlot 向前遍歷數組,直到 Entry 為空時停止遍歷。這一步的主要目的是查找 staleSlot 前面過期的 Entry 的數組下標 slotToExpunge。

  • 從 staleSlot 向后遍歷數組

  • 若 Entry 的 key 與給定的 key 相等,將該 Entry 與 staleSlot 下標的 Entry 互換位置。目的是為了讓新增的 Entry 放到它「應該」在的位置。

  • 若找不到相等的 key,說明該 key 對應的 Entry 不在數組中,將新值放到 staleSlot 位置。該操作其實就是處理哈希沖突的「線性探測」方法:當某個位置已被占用,向后探測下一個位置。

  • 若 staleSlot 前面存在過期的 Entry,則執行清理操作。

  • PS: 所謂 Entry「應該」在的位置,就是根據 key 的 threadLocalHashCode 與數組長度取余計算出來的位置,即 k.threadLocalHashCode & (len - 1) ,或者哈希沖突之后的位置,這里只是為了方便描述。

    • expungeStaleEntry:清理過期的 Entry

    //?staleSlot?表示過期的槽位(即?Entry?數組的下標) private?int?expungeStaleEntry(int?staleSlot)?{Entry[]?tab?=?table;int?len?=?tab.length;//?1.?將給定位置的?Entry?置為?nulltab[staleSlot].value?=?null;tab[staleSlot]?=?null;size--;//?Rehash?until?we?encounter?nullEntry?e;int?i;//?遍歷數組for?(i?=?nextIndex(staleSlot,?len);(e?=?tab[i])?!=?null;i?=?nextIndex(i,?len))?{//?獲取?Entry?的?keyThreadLocal<?>?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

  • 從給定位置的下一個開始向后遍歷數組

  • 若遇到 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?后面連續過期的?Entry,直到?Entry?為?null,返回該?Entry?的下標i?=?expungeStaleEntry(i);}}?while?(?(n?>>>=?1)?!=?0);return?removed; }

    該方法做了什么呢?從給定位置的下一個開始掃描數組,若遇到 key 為空的 Entry(過期的),則清理該位置及其后面過期的槽。

    值得注意的是,該方法循環執行的次數為 log(n)。由于該方法是在 set 方法內部被調用的,也就是新增/更新時:

  • 如果不掃描和清理,set 方法執行速度很快,但是會存在一些垃圾(過期的 Entry);

  • 如果每次都掃描清理,不會存在垃圾,但是插入性能會降低到 O(n)。

  • 因此,這個次數其實就一種平衡策略:Entry 數組較小時,就少清理幾次;數組較大時,就多清理幾次。

    • rehash:調整 Entry 數組

    private?void?rehash()?{//?清理數組中過期的?EntryexpungeStaleEntries();//?Use?lower?threshold?for?doubling?to?avoid?hysteresisif?(size?>=?threshold?-?threshold?/?4)resize(); }//?從頭開始清理整個?Entry?數組 private?void?expungeStaleEntries()?{Entry[]?tab?=?table;int?len?=?tab.length;for?(int?j?=?0;?j?<?len;?j++)?{Entry?e?=?tab[j];if?(e?!=?null?&&?e.get()?==?null)expungeStaleEntry(j);} }

    該方法主要作用:

  • 清理數組中過期的 Entry

  • 若清理后 Entry 的數量大于等于 threshold 的 3/4,則執行 resize 方法進行擴容

    • resize 方法:Entry 數組擴容

    /***?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?數組,將數組中的值移到新數組中for?(int?j?=?0;?j?<?oldLen;?++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?{//?計算在新數組中的位置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 數組擴容,主要流程:

  • 創建一個新數組,長度為原數組的 2 倍;

  • 從下標 0 開始遍歷舊數組的所有元素

  • 若元素已過期(key 為空),則將 value 也置空

  • 將未過期的元素移到新數組

  • 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 中獲取的值為空,執行 setInitialValue 方法

    • setInitialValue 方法:獲取/設置初始值

    private?T?setInitialValue()?{//?獲取初始值T?value?=?initialValue();Thread?t?=?Thread.currentThread();ThreadLocalMap?map?=?getMap(t);if?(map?!=?null)map.set(this,?value);elsecreateMap(t,?value);return?value; }protected?T?initialValue()?{return?null; }

    先取初始值,這個初始值默認為空(該方法是 protected,可以由子類初始化)。

  • 若 Thread 的 ThreadLocalMap 已初始化,則將初始值存入 Map

  • 否則,創建 ThreadLocalMap

  • 返回初始值

  • 除了初始值,其他邏輯跟 set 方法是一樣的,這里不再贅述。

    PS: 可以看到初始值是惰性初始化的。

    • getEntry:從 Entry 數組中獲取給定 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;elsereturn?getEntryAfterMiss(key,?i,?e); }//?key?未命中 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;?//?是要找的?key,返回if?(k?==?null)expungeStaleEntry(i);?//?Entry?已過期,清理?Entryelsei?=?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 方法的主要執行流程如下:

  • 獲取當前線程的 ThreadLocalMap

  • 以當前 ThreadLocal 做為 key,從 Map 中查找相應的 Entry,將 Entry 的 key 置空

  • 將該 ThreadLocal 對應的 Entry 置空,并向后遍歷清理 Entry 數組,也就是 expungeStaleEntry 方法的操作,前面已經分析過了,這里不再贅述。

  • 3.4.4 主要方法小結

    ThreadLocal 的主要方法 set、get 和 remove 前面已經分析過,這里簡單做個小結。

    set 方法

    • 以當前 ThreadLocal 為 key、新增的 Object 為 value 組成一個 Entry,放入 ThreadLocalMap,也就是 Entry 數組中。

    • 計算 Entry 的位置后

      • 若該槽為空,直接放到這里;并清理一些過期的 Entry,必要時進行擴容。

      • 當遇到散列沖突時,線性探測向后查找數組中為空的、或者已經過期的槽,用新值替換。

    get 方法

    • 以當前 ThreadLocal 為 key,從 Entry 數組中查找對應 Entry 的 value

      • 若 ThreadLocalMap 未初始化,則用給定初始值將其初始化

      • 若 ThreadLocalMap 已初始化,從 Entry 數組查找 key

    remove 方法:以當前 ThreadLocal 為 key,從 Entry 數組清理掉對應的 Entry,并且再清理該位置后面的、過期的 Entry

    方法雖少,但是稍微有點繞,除了做本身的功能,都執行了一些額外的清理操作。

    分析了這幾個方法的源碼之后,下面就來研究一下內存泄漏的問題。


    4. 內存泄漏分析

    首先說明一點,ThreadLocal 通常作為成員變量或靜態變量來使用(也就是共享的),比如前面應用場景中的例子。因為局部變量已經在同一條線程內部了,沒必要使用 ThreadLocal。

    為便于理解,這里先給出了 Thread、ThreadLocal、ThreadLocalMap、Entry 這幾個類在 JVM 的內存示意圖:

    簡單說明:

    • 當一個線程運行時,棧中存在當前 Thread 的棧幀,它持有 ThreadLocalMap 的強引用。

    • ThreadLocal 所在的類持有一個 ThreadLocal 的強引用;同時,ThreadLocalMap 中的 Entry 持有一個 ThreadLocal 的弱引用。

    4.1 場景一

    若方法執行完畢、線程正常消亡,則 Thread 的 ThreadLocalMap 引用將斷開,如圖:

    以后 GC 發生時,弱引用也會斷開,整個 ThreadLocalMap 都會被回收掉,不存在內存泄漏。

    4.2 場景二

    如果是線程池中的線程呢?也就是線程一直存活。經過 GC 后 Entry 持有的 ThreadLocal 引用斷開,Entry 的 key 為空,value 不為空,如圖所示:

    此時,如果沒有任何 remove 或者 get 等清理 Entry 數組的動作,那么該 Entry 的 value 持有的 Object 就不會被回收掉。這樣就產生了內存泄漏。

    這種情況其實也很容易避免,使用完執行 remove 方法就行了。

    5. 小結

    本文分析了 ThreadLocal 的主要方法實現,并分析了它可能存在內存泄漏的場景。

  • ThreadLocal 主要用于當前線程從共享變量中保存一份「副本」,常用的一個場景就是單點登錄保存用戶的登錄信息。

  • ThreadLocal 將數據存儲在 ThreadLocalMap 中,ThreadLocalMap 是由 Entry 構成的數組,結構有點類似 HashMap。

  • ThreadLocal 使用不當可能會造成內存泄漏。避免內存泄漏的方法是在方法調用結束前執行 ThreadLocal 的 remove 方法。

  • 有道無術,術可成;有術無道,止于術

    歡迎大家關注Java之道公眾號

    好文章,我在看??

    總結

    以上是生活随笔為你收集整理的ThreadLocal到底有没有内存泄漏?从源码角度来剖析一波的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

    主站蜘蛛池模板: 伊人久艹 | 毛色毛片| 九九久久免费视频 | 男人免费网站 | 午夜偷拍福利视频 | 日韩爱爱爱 | 日本高清一区二区视频 | 老妇裸体性猛交视频 | 久久精品亚洲无码 | 99视频免费| 亚洲激情免费 | 日韩精品久久久久久免费 | 久久久久久久久久久久久久久 | 玖玖在线播放 | 成年人晚上看的视频 | 国产av人人夜夜澡人人爽麻豆 | 精品三级国产 | 国产一区免费观看 | 人妻内射一区二区在线视频 | 欧美激情影音先锋 | 国产亚洲欧美一区二区 | 亚洲欧美在线免费 | 草久在线 | 无码人妻aⅴ一区二区三区日本 | 婷婷tv | 色女人在线 | 中文一区二区在线观看 | 国产精品va无码一区二区 | 欧美一级片在线视频 | 老司机精品福利导航 | 亚洲av无码乱码在线观看性色 | 在线射| 欧洲亚洲天堂 | www视频在线观看网站 | 国产aⅴ精品一区二区果冻 台湾性生生活1 | 国产精品毛片一区二区 | 国产裸体无遮挡 | 九九人人 | 打开免费观看视频在线 | 91国内产香蕉 | 国产欧美精品在线观看 | 免费毛片大全 | 男女三级视频 | 国产精品国产成人国产三级 | 精品人妻少妇一区二区三区 | 国产视频久久久久久久 | 一区二区三区四区五区av | 亚洲一级黄色片 | 在线观看国产视频 | 黑料视频在线 | 亚洲av无码成人精品国产 | 外国av在线 | 中文字幕一区二区在线视频 | 久综合 | 国产精品免费看 | 色噜av | 免费成人黄色片 | 四虎新网址 | www.一区二区.com | 2024国产精品 | 激情视频网站在线观看 | 人妖天堂狠狠ts人妖天堂狠狠 | 精品乱码一区二区三区 | 久久久久久久一 | 射网站| 国产乱强伦一区二区三区 | 欧美人妖69xxxxxhd3d | 麻豆久久久久久久 | 欧美一区二区久久 | 欧美三区在线观看 | 少妇在线视频 | 午夜一区在线 | 毛利兰被扒开腿做同人漫画 | julia在线播放88mav | 欧美日韩免费在线视频 | 波多野结衣免费视频观看 | 亚洲成人手机在线 | 教练含着她的乳奶揉搓揉捏动态图 | 欧美影视一区二区三区 | 亚洲精品66 | 国产人妖ts | 精品国产18久久久久久 | 色婷婷av一区二区三区四区 | 欧美国产综合视频 | av福利社| 美女黄色一级视频 | 欧洲精品在线播放 | 久久看毛片| 国内精品久久久久 | 中文字幕第99页 | 5566色| 少妇特黄a一区二区三区88av | 同人动漫在线观看 | 五月婷婷伊人网 | 古装三级吃奶做爰 | 黄色免费视频观看 | 日韩精品一区二区三区色欲av | 国产婷婷色一区二区在线观看 | 久久手机免费视频 |