目錄結構java
一、應用場景及做用 二、結構關係 2.一、三者關係類圖 2.二、ThreadLocalMap結構圖 2.三、 內存引用關係 2.四、存在內存泄漏緣由 三、源碼分析 3.一、重要代碼片斷 3.二、重要方法分析 3.三、set(T): void 3.四、get():T 3.五、remove():void 3.六、總結
-1做用、ThreadLocal 爲了實現線程之間數據隔離,每一個線程中有獨立的變量副本,操做互不干擾。區別於線程同步中,同步在爲了保證正確使用同一個共享變量,須要加鎖。 -2應用場景: 1)能夠對一次請求過程,作一個過程日誌追蹤。如slf4j的MDC組件的使用,能夠在日誌中每次請求過程加key,方便定位一次請求流程問題。 2)解決線程中全局數據傳值問題。
要理清ThreadLocal的原理做用,能夠先了解Thread, ThreadLocal, ThreadLocalMap三者之間的關係。簡單類圖關係以下算法
一、Thread 類中有ThreadLocalMap類型的成員變量 threadLocals 二、ThreadLocalMap是ThreadLocal的靜態內部類 三、Thread 與 ThreadLocal怎麼關聯? 線程對象中threadLocals中存儲的鍵值對 key--> ThreadLocal對象,value --> 線程須要保存的變量值
ThreadLocalMap 底層實現實質是一個Entry對象數組, 默認容量是16,在存儲元素到數組中,本身實現了一個算法來尋址(計算數組下標), 與Map集合中的HashMap有所不一樣。 Entry對象中 key是ThreadLocal對象。 誤區:在不瞭解原理前,會想線程之間要實現數據隔離,那這個集合中key應該是Thread對象,這樣在存的時候,以當前線程對象爲key,value爲要保存的值,這樣在獲取的時候,經過線程對象去get獲取相應的值。
-1,同一個ThreadLocal對象可被多個線程引用,每一個線程之間本地變量副本存儲,實現數據獨立性,可見每一個線程內部都有單獨的map集合,即便引用的ThreadLocal同一個,value能夠不一樣,如圖中ThreadLocal1對象,同時被線程A,B引用做爲key -2,一個線程能夠存儲多個ThreadLocal,因線程中存儲的只能存儲同一個ThreadLocal對象一次,再次存儲相同的Threadlocal對象,由於key相同,會覆蓋原來的value,value能夠是基本數據類型的值,也能夠是引用數據類型(如封裝的對象)
ThreadLocal對象沒有外部強引用後,只存在弱引用,下一次GC會被回收。以下:數組
-1,上圖實線箭頭表明強引用,虛線表明弱引用; JVM存在四種引用:強引用,軟引用,弱引用,虛引用,弱引用對象,會在下一次GC(垃圾回收)被回收。 -2,上圖可見Entry的 Key指向的ThreadLocal對象的引用是弱引用,一旦tl的強引用斷開,沒有外部的強引用後,在下一次JVM垃圾回收時,ThreadLocal對象被回收了,此時 key--> null,而此時 Entry對象,是有一條強引用鏈的,th--> Thread對象-->ThreadLocalMap--> Entry,可達性性分析是可達的,這時ThreadLocalMap集合,即在數組的某一個索引是有Entry引用的,可是該Entry的key爲null,value依然有值,但再也用不了了,這時的Entry稱爲staleEntry(我理解爲失效的Entry),形成內存泄漏。 -3,內存泄漏是指分配的內存,gc回收不了,本身也用不了; 內存溢出,是指內存不夠,若有剩餘2M內存,這時有一個對象建立須要3M,內存不夠,致使溢出。內存泄漏可能會致使內存溢出,由於內存泄漏就會有人佔着茅坑不拉屎,可用空間愈來愈少,gc也回收不了,最終致使內存溢出。 -4,那線程對象被回收了,這條引用鏈斷了就沒事了,下次Gc就會把ThreadLocalMap集合中對象所有回收了,就不存在內存泄漏問題了;但開發環境,線程通常會在線程池建立來節約資源,每一個線程是被重複使用的,生命週期很長,線程對象長時間是存在內存中的,而ThreadLocalMap和Thread生命週期相同,只有線程結束,它內部持有的ThreadLocalMap對象纔會銷燬,以下Thread#exit: private void exit() { if (group != null) { group.threadTerminated(this); group = null; } /* Aggressively null out all reference fields: see bug 4006245 */ target = null; //線程退出時,才斷開ThreadLocalMap引用 threadLocals = null; inheritableThreadLocals = null; inheritedAccessControlContext = null; blocker = null; uncaughtExceptionHandler = null; }
本次源碼分析,主要分析ThreadLocal的set,get,remove三個方法,分別以此爲入口,一步步深刻每一個代碼方法查看其實現原理,在這分析以前,先撿幾個我理解比較重要的方法或者代碼片斷先解釋一下,有一個初步的理解,後面會更順暢。源碼分析
//1、ThreadLocalMap的尋址,因其底層是數組,在存放元素如何定位索引i存儲? //兩個要求:1)求的索引位置必定要在數組大小內 // 2)索引足夠均分分散,要求hashcode足夠散列,目的減小hash衝突。 //firstKey.threadLocalHashCode,就是爲了達到要求2,均分分散 // &(INITIAL_CAPACITY - 1) 爲了落在數組範圍內,經常使用進行模運算,這裏是巧妙運用位運算,效率更高, %2^n與 &(2^n-1)等價,因此要求數組的容量要爲2的冪; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); // -------------------> firstKey.threadLocalHashCode, //傳入的ThreadLocal對象,作了 0x61c88647的增量後求得hash值,爲何要加0x61c88647呢,與斐波那契數列有關,反正是一個神奇的魔法值,目的就是使的hash值更分散,減小hash衝突。 private final int threadLocalHashCode = nextHashCode(); private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } //2、如何ThreadLocalMap中,出現hash衝突了,即2個ThreadLocal對象的hash計算出來是相同的下標,這裏解決hash衝突使用線性探測法,即這個位置衝突,就尋找下一個位置,若是到數組終點了呢,從0再開始,因此這裏數組邏輯上是一個首尾相接的環形數組。 //1,向後遍歷,獲取索引位置 private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); } //2,向前遍歷 private static int prevIndex(int i, int len) { return ((i - 1 >= 0) ? i - 1 : len - 1); } 以下圖:
cleanSomeSlots 原理圖1以下:this
//一,分析清理失效Entry方法,清理起始位置是staleSlot //已經知道某個Entry的key==null了,那麼數組該位置的引用應該被清除 private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; //Entry的value引用也清除,方便gc回收 tab[staleSlot].value = null; // 清理數組當前位置的Entry tab[staleSlot] = null; size--; // Rehash until we encounter null Entry e; int i; //向後循環遍歷,直到遇到 null //作2件事: //1)遇到其餘失效Entry,順手清除 //2)沒有失效的Entry,從新hash一下,安排新位置;由於可能以前某些位置有hash衝突,致使根據key生成hash的值與當前的位置i不一致(衝突,會日後順延,這裏是邏輯上日後,達到數組長度,從0開始),而這時又清理了很多失效的Entry,可能會有空位了,因此從新hash調一下順序,提升效率。 for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == null) { //1,失效Entry,清除 e.value = null; tab[i] = null; size--; } else { int h = k.threadLocalHashCode & (len - 1); //2,hash值與當前數組索引位置不一樣 if (h != i) { tab[i] = null; //3,向後遍歷,找合適空位置插入 while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } //返回i位置, Entry ==null return i; } //2、可伸縮性遍歷某段範圍失效的Entry cleanSomeSlots(int i, int n),原理如上圖1 //爲何要有伸縮性,我理解仍是爲了效率,若是發現這範圍內有須要清理的失效Entry,才把範圍放大一些查找清除,源代碼以下: 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,當n傳入的是size,即數組實際容納的數量,n擴大到數組長度了, //影響在於,原來清理遍歷的只是數組的一個小範圍,一會兒擴大到了整個數組。我理解這樣作爲了提升執行效率,沒有檢測到失效entry就小範圍清理一下,檢測到就大範圍清理。 if (e != null && e.get() == null) { n = len; removed = true; i = expungeStaleEntry(i); } // n >>>= 1,即 n向右位移1位,即 n/2, 可循環次數log2n次 } while ( (n >>>= 1) != 0); return removed; } //3、當在set時,發現當前生成的數組位置已經被其餘Entry佔了,可是它失效了,key==null,這時須要把它給替換了吧,replaceStaleEntry(ThreadLocal<?> key, Object value,int staleSlot),較難理解,多看幾遍哈,原理圖看下面,分析了2種狀況,還有先後遍歷都發現有失效的Entry狀況,請自行腦補了哈。 //源代碼以下: private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; // 1,要清除的位置 int slotToExpunge = staleSlot; //2,從i 向前遍歷,找到左邊第一個失效的位置(指的是Entry !=null,key==null) for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e.get() == null) slotToExpunge = i; //3,從i向後遍歷 for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); //4,傳入 staleSlot的 key==null,是一個失效的Entry, 從staleSlot+1個向後遍歷,若是 //碰見 k==key,將staleSlot索引位置與此處i替換位置,即將失效的Entry日後面放 if (k == key) { e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; // Start expunge at preceding stale entry if it exists //若是相等,staleSlot左邊沒有失效的entry,賦值爲此處i,此處已經替換爲失效Entry了,若是不相等,那麼就清除失效Entry,以staleSlot最左邊那一個失效entry開始清除 if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); //cleanSomeSlots的目的:expungeStaleEntry返回的是entry ==null的索引i, //清理i到len這一段的失效entry,中間會有null的狀況嗎? return; } //5,slotToExpunge == staleSlot 表示 左邊沒有失效entry, 右邊碰見第一失效entry,標記此處索引,以便後文肯定從哪裏開始清除無效entry if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } //解除value的引用,gc會回收 tab[staleSlot].value = null; //數組失效Entry位置,賦值新的Entry tab[staleSlot] = new Entry(key, value); //6,不相等,確定有失效索引須要清理,執行清除 if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }
replaceStaleEntry 情景圖1以下:線程
replaceStaleEntry 情景圖2以下:debug
set 方法,是代碼最多的,也是最重要的。其中expungeStaleEntry, replaceStaleEntry,cleanSomeSlots 三個方法較爲主要,目的是找出、清理失效的Entry的過程,其中replaceStaleEntry 較難理解。設計
//1、從 set() 着手,入口 public void set(T value) { //1,獲取當前線程對象 Thread t = Thread.currentThread(); //2,獲取當前線程的map, 每一個線程持有一個threadLocals對象,經過該map來實現線程之間數據的隔離,達到每一個線程擁有本身獨立的局部變量。 見代碼分析二 ThreadLocalMap map = getMap(t); if (map != null) // 見代碼分析四 map.set(this, value); else //3,若是當前線程持有的map爲空,建立map,見代碼分析三 createMap(t, value); } //2、 獲取ThreadLocalMap 方法,獲取當前線程持有的map對象 ThreadLocalMap getMap(Thread t) { return t.threadLocals; } // Thread類中持有hreadLocalMap類型的對象,該map是ThreadLocal的靜態內部類 ThreadLocal.ThreadLocalMap threadLocals = null; //3、代碼分析 void createMap(Thread t, T firstValue) { // 建立 map對象,下見ThreadLocalMap的構造方法,很關鍵,該map與經常使用的HashMap等不一樣 t.threadLocals = new ThreadLocalMap(this, firstValue); } //構造方法,ThreadLocalMap 可以實現key-value的map集合結構,底層實際是一個數組,Entry爲其每一個節點對象,Entry 包含key和value ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { //1,初始化容量爲16的Entry[]數組 table = new Entry[INITIAL_CAPACITY]; //2,這一步目的就是根據傳入的ThreadLocal對象做爲key,爲了求放在數組下的索引位置,肯定放在哪 //兩個要求:1)求的索引位置必定要在數組大小內(這裏即0-15範圍) // 2)索引足夠均分分散,要求hashcode足夠散列,目的減小hash衝突。 //firstKey.threadLocalHashCode,就是爲了達到要求2 // &(INITIAL_CAPACITY - 1) 爲了落在數組範圍內,經常使用進行模運算,這裏是巧妙運用位運算,效率更高, %2^n與 &(2^n-1)等價 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //求得索引位置,放入數組中 table[i] = new Entry(firstKey, firstValue); size = 1; //設置數組容量閾值,即填充因子,用於後續判斷是否須要擴容 setThreshold(INITIAL_CAPACITY); } //爲了使傳入的ThreadLocal對象求在數組索引位置,求的其hashcode,加上了0x61c88647增量,目的是爲了足夠分散 private static final int HASH_INCREMENT = 0x61c88647; /** * Returns the next hash code. */ private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } //設置數組擴容閾值 private void setThreshold(int len) { //初始填充因子爲2/3,數組容量的2/3,即 16*2/3=10 threshold = len * 2 / 3; } //4、代碼分析 set(key,value) private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; //求數組索引位置 int i = key.threadLocalHashCode & (len-1); //這裏用for循環,是爲了解決hash衝突時,查找下一個可用 slot(卡槽,位置; 即生成的索引i,發現已有Entry佔用了,找下一個位置插入,這裏解決hash衝突方式不一樣於hashmap的拉鍊法(在衝突位置,以鏈表形式串接),這裏採用的是線性尋址法,即數組當前i位置被佔用了,看第i+1個位置,若是i+1已經大於等於數組length,再從數組下標0 從頭開始,從該i = nextIndex(i, len)可知道是邏輯上這是一個首尾循環式數組) for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { //1,從i向後遍歷,若Entry爲null,跳出循環; 不爲null,獲取Entry的key,線程初始化 threadLocalMap集合就有3個 Entry(此處不解?debug看了) ThreadLocal<?> k = e.get(); //2,若是數組當前位置key與將要設值的 threadlocal對象相等,覆蓋原value,返回 if (k == key) { e.value = value; return; } if (k == null) { //3,若是數組當前位置key爲空,須要替換失效的Entry(stale:不新鮮的,Entry的key ==null) //見代碼分析五 replaceStaleEntry(key, value, i); return; } } //4,若是上面for循環,出現hash衝突了,跳出循環,此時索引i位置 Entry==null,在此插入新Entry tab[i] = new Entry(key, value); int sz = ++size; //5,cleanSomeSlots,順便清理一下失效的Entry,避免內存泄漏,見代碼分析六 if (!cleanSomeSlots(i, sz) && sz >= threshold) // 6,清理失敗且當前數組的Entry數量達到設定閾值了,執行 rehash,見代碼分析八 rehash(); } //5、 分析 replaceStaleEntry(key, value, i) 替換失效的Entry private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; // 1,要清除的位置 int slotToExpunge = staleSlot; //2,從i 向前遍歷,找到左邊第一個失效的位置(指的是Entry !=null,key==null) for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e.get() == null) slotToExpunge = i; //3,從i向後遍歷 for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); //4,傳入 staleSlot的 key==null,是一個失效的Entry, 從staleSlot+1個向後遍歷,若是 //碰見 k==key,將staleSlot索引位置與此處i替換位置,即將失效的Entry日後面放, if (k == key) { e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; // Start expunge at preceding stale entry if it exists //若是相等,staleSlot左邊沒有失效的entry,賦值爲此處i,此處已經替換爲失效Entry了,若是不相等,那麼就清除失效Entry,以staleSlot最左邊那一個失效entry開始清除 if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); //cleanSomeSlots的目的:expungeStaleEntry返回的是entry ==null的索引i, //清理i到len這一段的失效entry,中間會有null的狀況嗎? return; } //5,slotToExpunge == staleSlot 表示 左邊沒有失效entry, 右邊碰見第一失效entry,標記此處索引,以便後文肯定從哪裏開始清除無效entry if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } //解除value的引用,gc會回收 tab[staleSlot].value = null; //數組失效Entry位置,賦值新的Entry tab[staleSlot] = new Entry(key, value); //6,不相等,確定有失效索引須要清理,執行清除 if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); } //6、分析 cleanSomeSlots(int i, int n),清理某些失效的Entry方法 //i爲 失效位置, n分2種傳入場景 // 1)數組的實際Entry數量 size // 2) 數組的容量 length 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,當n傳入的是size,即數組實際容納的數量,n擴大到數組長度了, //影響在於,原來清理遍歷的只是數組的一個小範圍,一會兒擴大到了整個數組。我理解這樣作爲了提升執行效率,沒有檢測到失效entry就小範圍清理一下,檢測到就大範圍清理。 if (e != null && e.get() == null) { n = len; removed = true; i = expungeStaleEntry(i); } // n >>>= 1,即 n向右位移1位,即 n/2, 可循環次數log2n次 } while ( (n >>>= 1) != 0); return removed; } //7、分析清理失效Entry方法,清理起始位置是staleSlot private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // 清理當前位置的Entry tab[staleSlot].value = null; tab[staleSlot] = null; size--; // Rehash until we encounter null Entry e; int i; //向後循環遍歷,直到遇到 null //作2件事: //1)遇到其餘失效Entry,順手清除 //2)沒有失效的Entry,從新hash一下,安排新位置;由於可能以前某些位置有hash衝突,致使根據key生成hash的值與當前的位置i不一致(衝突,會日後順延,這裏是邏輯上日後,達到數組長度,從0開始),而這時又清理了很多失效的Entry,可能會有空位了,因此從新hash調一下順序,提升效率。 for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == null) { //1,失效Entry,清除 e.value = null; tab[i] = null; size--; } else { int h = k.threadLocalHashCode & (len - 1); //2,hash值與當前數組索引位置不一樣 if (h != i) { tab[i] = null; //3,向後遍歷,找合適空位置插入 while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } //返回i位置, Entry ==null return i; } //8、rehash //首先掃描全表,清除全部失效的Entry, 若是這還不能充分地縮小數組的大小,擴容爲當前的2倍 private void rehash() { //1,清除全部失效的entry,見代碼分析九 expungeStaleEntries(); //2,threshold = length * 2/ 3 //size >= threshold - threshold / 4 = threshold*3/4 , //即size >= length *2/3 *3/4= length* 1/2, 只要數組的大小>=於數組容量的一半,就擴容。 if (size >= threshold - threshold / 4) //見代碼分析十 resize(); } //9、遍歷數組所有節點,清除失效的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); } } //10、擴容爲原來的2倍 private void resize() { Entry[] oldTab = table; int oldLen = oldTab.length; int newLen = oldLen * 2; Entry[] newTab = new Entry[newLen]; int count = 0; // 舊數組數據向新數組遷移,順便清除失效的entry的value,幫助Gc容易發現它,直接回收 //Entry不清除了嗎?這裏舊數組以後就沒有被人引用了,下次Gc會直接回收 for (int j = 0; j < oldLen; ++j) { Entry e = oldTab[j]; if (e != null) { ThreadLocal<?> k = e.get(); 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; }
//1、從get() 着手 public T get() { //1,獲取當前線程對象 Thread t = Thread.currentThread(); //2,獲取該線程的 map集合,每一個線程都有單獨的map ThreadLocalMap map = getMap(t); if (map != null) { //3,this指的是 ThreadLocal對象,以它爲key,去map中獲取相應的Entry, //易混淆:ThreadLocalMap 中存儲的Entry鍵值對,key是ThreadLocal對象,而不是線程對象。 //此處 map.getEntry(this) 下面代碼二 分析 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { //4,返回value @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } //該線程若沒有map對象, 返回初始默認值,詳見代碼分析四 return setInitialValue(); } // 2、分析 map.getEntry(this) private Entry getEntry(ThreadLocal<?> key) { //1,獲取數組索引位置 int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) //2,直接就命中,沒有hash衝突,返回 return e; else //3,遍歷其餘Entry,見代碼分析三 return getEntryAfterMiss(key, i, e); } //3、根據key獲取Entry,沒有直接命中,繼續遍歷查找 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) //1,命中返回,爲啥重複判斷一次? 由於這是在while循環,會日後執行再判斷 return e; if (k == null) //2,當前位置Entry失效,清除 expungeStaleEntry(i); else //3,hash衝突,獲取下一個索引 i = nextIndex(i, len); e = tab[i]; } //4,數組中沒有找到該key return null; } //4、沒有map,返回默認值,初始化操做 private T setInitialValue() { //1,調用默認的初始化方法, 以下,通常用來被重寫的,給定一個初始值 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; }
// 1、入口 public void remove() { //1,獲取當前線程的map集合 ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) //2,見代碼分析二 m.remove(this); } // 2、 m.remove(this); private void remove(ThreadLocal<?> key) { Entry[] tab = table; int len = tab.length; //一、獲取該key在數組中索引位置 int i = key.threadLocalHashCode & (len-1); //2,從i位置向後循環判斷,考慮hash衝突 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { //3,找到該key, if (e.get() == key) { //4,引用置空 e.clear(); // 5,從i開始清除失效的Entry,避免內存泄漏 expungeStaleEntry(i); return; } } } //引用置空 public void clear() { this.referent = null; }
從set,get,remove代碼可見,每一個方法都會去清除失效的Entry,說明設計者也考慮到內存泄漏的問題,因此建議在使用完ThreadLocal,及時執行remove方法清除一下,避免潛在的內存泄漏問題。