<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          ThreadLocal到底有沒有內存泄漏?

          共 12801字,需瀏覽 26分鐘

           ·

          2020-11-03 12:01

          點擊上方藍色“程序猿DD”,選擇“設為星標”

          回復“資源”獲取獨家整理的學習資料!

          1. 前言

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

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

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

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

          2. 應用場景

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

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

          • 若未登錄,跳轉到登錄頁面;
          • 若已登錄,獲取并保存用戶的登錄信息。

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

          public?class?UserInfoHolder?{
          ????private?static?final?ThreadLocal>?USER_INFO_THREAD_LOCAL?=?new?ThreadLocal<>();

          ????public?static?void?set(Map?map)?{
          ????????USER_INFO_THREAD_LOCAL.set(map);
          ????}
          ????
          ????public?static?Map?get()?{
          ????????return?USER_INFO_THREAD_LOCAL.get();
          ????}
          ????
          ????public?static?void?clear()?{
          ????????USER_INFO_THREAD_LOCAL.remove();
          ????}
          ????
          ????//?...
          }

          通過 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?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ū)別:

          1. HashMap 處理哈希沖突使用的「鏈表法」。也就是當產生沖突時拉出一個鏈表,而且 JDK 1.8 進一步引入了紅黑樹進行優(yōu)化。
          2. 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ù)組下標:

          1. 如果數(shù)組下標的 Entry 不為空,表示該位置已經有元素。由于可能存在哈希沖突,因此這個位置的元素可能并不是要找的元素,所以遍歷數(shù)組去比較
            1. 如果找到等于當前 key 的 Entry,則用新值替換舊值,返回。
            2. 如果遍歷過程中,遇到 Entry 不為空、但是 Entry 的 key 為空的情況,則會做一些清理工作。
          2. 如果數(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í)行流程如下:

          1. 從 staleSlot 向前遍歷數(shù)組,直到 Entry 為空時停止遍歷。這一步的主要目的是查找 staleSlot 前面過期的 Entry 的數(shù)組下標 slotToExpunge。
          2. 從 staleSlot 向后遍歷數(shù)組
            1. 若 Entry 的 key 與給定的 key 相等,將該 Entry 與 staleSlot 下標的 Entry 互換位置。目的是為了讓新增的 Entry 放到它「應該」在的位置。
            2. 若找不到相等的 key,說明該 key 對應的 Entry 不在數(shù)組中,將新值放到 staleSlot 位置。該操作其實就是處理哈希沖突的「線性探測」方法:當某個位置已被占用,向后探測下一個位置。
          3. 若 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;
          }

          該方法主要做了哪些工作呢?

          1. 清空給定位置的 Entry
          2. 從給定位置的下一個開始向后遍歷數(shù)組
            1. 若遇到 Entry 為 null,結束遍歷
            2. 若遇到 key 為空的 Entry(即過期的),就將該 Entry 置空
            3. 若遇到 key 不為空的 Entry,而且經過計算,該 Entry 并不在它「應該」在的位置,則將其移動到它「應該」在的位置
          3. 返回 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 方法內部被調用的,也就是新增/更新時:

          1. 如果不掃描和清理,set 方法執(zhí)行速度很快,但是會存在一些垃圾(過期的 Entry);
          2. 如果每次都掃描清理,不會存在垃圾,但是插入性能會降低到 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);
          ????}
          }

          該方法主要作用:

          1. 清理數(shù)組中過期的 Entry
          2. 若清理后 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ù)組擴容,主要流程:

          1. 創(chuàng)建一個新數(shù)組,長度為原數(shù)組的 2 倍;
          2. 從下標 0 開始遍歷舊數(shù)組的所有元素
            1. 若元素已過期(key 為空),則將 value 也置空
            2. 將未過期的元素移到新數(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 并判斷:

          1. 若 Map 已存在,從 Map 中取值
          2. 若 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,可以由子類初始化)。

          1. 若 Thread 的 ThreadLocalMap 已初始化,則將初始值存入 Map
          2. 否則,創(chuàng)建 ThreadLocalMap
          3. 返回初始值

          除了初始值,其他邏輯跟 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í)行流程如下:

          1. 獲取當前線程的 ThreadLocalMap
          2. 以當前 ThreadLocal 做為 key,從 Map 中查找相應的 Entry,將 Entry 的 key 置空
          3. 將該 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),并分析了它可能存在內存泄漏的場景。

          1. ThreadLocal 主要用于當前線程從共享變量中保存一份「副本」,常用的一個場景就是單點登錄保存用戶的登錄信息。
          2. ThreadLocal 將數(shù)據(jù)存儲在 ThreadLocalMap 中,ThreadLocalMap 是由 Entry 構成的數(shù)組,結構有點類似 HashMap。
          3. ThreadLocal 使用不當可能會造成內存泄漏。避免內存泄漏的方法是在方法調用結束前執(zhí)行 ThreadLocal 的 remove 方法。

          本文內容就到這里,若有不正之處歡迎指正,覺得有所收獲歡迎三連支持~


          往期推薦

          程序員編碼時都戴耳機?到底在聽什么?

          Spring Data 發(fā)布更改版本管理方案之后的第一個版本:2020.0.0

          終于還是對“帶薪拉SHI”出手了...

          Spring 5.3 正式GA,維護至2024年,4.3版本年末結束維護

          聊聊訂單系統(tǒng)的設計?


          掃一掃,關注我

          一起學習,一起進步

          每周贈書,福利不斷

          深度內容

          推薦加入


          最近熱門分享話題:?#滬牌代拍的技術、策略與設計

          瀏覽 33
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                    <th id="afajh"><progress id="afajh"></progress></th>
                    伊人成人在线观看 | 全球亚洲精品视频 | 大雞巴弄得我好舒服黃片动漫版 | 日韩日逼网 | 青草网视频|