ThreadLocal

      網友投稿 588 2025-04-01

      1、簡介

      ThreadLocal也稱線程變量,它是一個以ThreadLocal對象為鍵、任意對象為值的存儲結構(ThreadLocal中ThreadLocalMap的Entry結構),這個結構會被附帶在線程上,以此來做線程數據的隔離。ThreadLocal是維持線程的封閉性的一種規范,它提供set()/get()等方法維護和訪問線程中存儲的私有副本,ThreadLocal通常用于防止對可變的單實例變量或者全局變量進行共享。

      ThreadLocal和synchronized兩者經常會被拿出來一起討論,雖然二者都是用來解決多線程中資源的訪問沖突等問題,但是二者存在本質上的區別具有完全不一樣的使用場景。這里簡單說明一下:

      synchronized是通過線程阻塞(加鎖),只允許同步區域內同一時刻只有一個線程在執行來實現共享資源的互斥訪問,犧牲了程序的執行時間

      ThreadLocal是每個線程具有不同的數據副本,通過線程數據隔離互不影響的方式來解決并發資源的訪問,犧牲的是存儲空間

      相比之下ThreadLocal的使用場景比較特殊,在某些需要以線程為作用域做資源隔離的場景下使用,比如應用程序中以線程為單位發起的數據庫連接,可以通過將JDBC的連接保存到ThreadLocal對象中來保證線程安全。

      ThreadLocal的簡單使用示例:

      package com.liziba.tl; /** *

      * ThreadLocal demo -> 線程隔離 *

      * * @Author: Liziba */ public class ThreadLocalDemo { ThreadLocal threadLocal = ThreadLocal.withInitial(() -> Integer.valueOf(10)); public static void main(String[] args) { for (int i = 0; i < 10; i++) { new Thread(() -> run(), "Thread-" + i).start(); } } public static void run() { ThreadLocalDemo local = new ThreadLocalDemo(); local.threadLocal.set(local.threadLocal.get() + 5); System.out.println(Thread.currentThread().getName() +" : "+local.threadLocal.get()); } }

      輸出結果:

      上述運行結果可以看到線程并行運行,但線程各自擁有資源副本,彼此之間互不影響,是線程安全的。

      2、Thread、ThreadLocal、ThreadLocalMap三者的關系

      在進行源碼分析和原理講解之前,有必要先了解這三者之間的關系。Thread、ThreadLocal、ThreadLocalMap這三者從命名都包含一個Thread那么它們具體是什么關系呢?接下來通過一些重要的代碼片段和圖示來闡述三者之間的關系,并且也會介紹到ThreadLocal、ThreadLocalMap中的一些重要屬性和數據結構。

      java.lang.Thread 中的代碼片段:

      public class Thread implements Runnable { /** Thread中持有一個ThreadLocal中的ThreadLocalMap */ ThreadLocal.ThreadLocalMap threadLocals = null; }

      java.lang.ThreadLocal中的代碼片段:

      public class ThreadLocal { /** * ThreadLocalMap 是ThreadLocal的靜態內部類 */ static class ThreadLocalMap { // 在下面 } }

      java.lang.ThreadLocal.ThreadLocalMap中的代碼片段:

      static class ThreadLocalMap { /** 默認的初始Entry的大小 */ private static final int INITIAL_CAPACITY = 16; /** 定義一個Entry數組,用來存放多個ThreadLocal */ private Entry[] table; /** 數組擴容因子 */ private int threshold; /** 記錄table中Entry的個數 */ private int size = 0; /** * ThreadLocalMap中有靜態內部類Entry,Entry繼承了WeakReference弱引用,引用類型是ThreadLocal */ static class Entry extends WeakReference> { /** The value associated with this ThreadLocal. */ Object value; /** * key是ThreadLocal對象 * value是ThreadLocal中的value */ Entry(ThreadLocal k, Object v) { super(k); value = v; } } /** * ThreadLocalMap的構造函數 */ ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { // 初始化數組,默認16 table = new Entry[INITIAL_CAPACITY]; // 通過一定的算法計算ThreadLocal在table數組中的索引 -> 這個3.2中我做了詳細講解 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); // 賦值table[i]位置Entry對象key -> ThreadLocal | value -> 傳入的值 table[i] = new Entry(firstKey, firstValue); // 記錄 table中Entry的個數 size = 1; // 計算擴容因子 16 * 2 / 3 setThreshold(INITIAL_CAPACITY); } }

      看了上述的代碼和代碼的注釋,可以很明確的看到Thread、ThreadLocal、ThreadLocalMap這三者關系

      Thread線程類內部維護了一個ThreadLocalMap成員變量(ThreadLocalMap的實例)

      ThreadLocalMap是ThreadLocal的靜態內部類,此外ThreadLocalMap內部維護了一個Entry數組table,用來存放多個ThreadLocal

      ThreadLocal類用于存儲以線程為作用域的數據,用于數據隔離

      從這張圖能非常清晰的看出,ThreadLocal只是ThreadLocalMap操作的一個入口,它提供的set()/get()方法供程序員開發使用,具體的數據存取都是在ThreadLocalMap中去實現,而每一個Thread對象中持有一個ThreadLocalMap對象,不難看出ThreadLocalMap才是實現的關鍵和重難點。

      3、ThreadLocal源碼分析

      ThreadLocal是JDK提供給程序員直接使用的類,其重點在于ThreadLocalMap,因此下面主要介紹ThreadLocal的關鍵成員屬性、如何通過魔數計算散列均勻的索引、get()/set()方法。重點將在ThreadLocalMap中去介紹。

      ThreadLocal中有幾個重要的成員屬性如下所示:

      /** 定義數組的初始大小 */ private static final int INITIAL_CAPACITY = 16; /** 魔數 -> 可以讓生成出來的值或者說ThreadLocal的Index均勻的分布在2^n的數組大小中 */ private static final int HASH_INCREMENT = 0x61c88647; /** 魔數 */ private final int threadLocalHashCode = nextHashCode(); /** 定義一個線程安全的原子類AtomicInteger,用于魔數的累加 */ private static AtomicInteger nextHashCode = new AtomicInteger();

      nextHashCode()方法:

      /** 計算下一個code(魔數累加) */ private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); }

      上面的魔數與斐波拉契散列有關,它可以讓生成出來的值或者說ThreadLocal在table的Index均勻的分布在2^n的數組大小中,我們通過計算的值再取模數組的length-1,就能得到ThreadLocal在ThreadLocalMap的Entry中的索引下標。下面通過自己寫一個測試案例來簡單的講述下這個魔數和計算數組索引:

      package com.lizba.currency.threadlocal; import java.util.concurrent.atomic.AtomicInteger; /** *

      * 通過魔數0x61c88647來計算數組索引下標 *

      * * @Author: Liziba * @Date: 2021/7/2 22:02 */ public class ThreadLocal0x61c88647 { /** 定義數組的初始大小 */ private static final int INITIAL_CAPACITY = 16; /** 魔數 -> 可以讓生成出來的值或者說ThreadLocal的Index均勻的分布在2^n的數組大小中 */ private static final int HASH_INCREMENT = 0x61c88647; /** 魔數 */ private final int threadLocalHashCode = nextHashCode(); /** 定義一個線程安全的原子類AtomicInteger,用于魔數的累加 */ private static AtomicInteger nextHashCode = new AtomicInteger(); /** 計算下一個code(魔數累加) */ private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } /** * 根據生成的均勻分布的隨機數threadLocalHashCode 取模(%) (數組大小INITIAL_CAPACITY-1(因為數組索引從0開始)) * * @return */ public int index() { return this.threadLocalHashCode & (INITIAL_CAPACITY - 1); } }

      測試上述代碼:

      public static void main(String[] args) { // 輸出16次,模擬ThreadLocal中的默認初始大小 for (int i = 0; i < 16; i++) { ThreadLocal0x61c88647 demo = new ThreadLocal0x61c88647(); System.out.println(demo.index()); } }

      輸出結果:

      魔數計算數組索引下標順序圖示:

      public void set(T value) { // 獲取當前線程 Thread t = Thread.currentThread(); // 獲取當前線程的ThreadLocalMap -> getMap(t)方法在下面 ThreadLocalMap map = getMap(t); // ThreadLocalMap不為空,表示已經初始化 -> 這里兩個分支是ThreadLocal的重點 if (map != null) map.set(this, value); // 直接設值,key為當前ThreadLocal對象,value為set傳入的值T else createMap(t, value); // 為空則需要初始化,再設值 }

      獲取當前線程的ThreadLocalMap -> getMap(Thread t)

      /** * java.lang.Thread類中定義了ThreadLocal.ThreadLocalMap threadLocals = null; * t.threadLocals為獲取當前線程對象的ThreadLocalMap */ ThreadLocalMap getMap(Thread t) { // 獲取當前線程的ThreadLocalMap return t.threadLocals; }

      ThreadLocalMap為空初始化 -> createMap(t, value)

      void createMap(Thread t, T firstValue) { // 實例化一個ThreadLocalMap,賦值給當前線程的threadLocals成員變量 // new ThreadLocalMap(this, firstValue) -> 源碼分析放到后面ThreadLocalMap中去講解,這里只需要明白這是初始化一個ThreadLocalMap即可,加上第二節中三者的說明,也能理解其中的原理。 t.threadLocals = new ThreadLocalMap(this, firstValue); }

      public T get() { // 獲取當前線程t Thread t = Thread.currentThread(); // 獲取當選線程的ThreadLocalMap -> 下面貼出了代碼 ThreadLocalMap map = getMap(t); // 如果不為空 if (map != null) { // 從ThreadLocalMap的table中取出Entry ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") // 返回entry 存儲的值 (entry key為ThreadLocal對象,value為存儲的值) T result = (T)e.value; return result; } } // 初始化ThreadLocalMap或構建value為null一個entry到table中 // 具體邏輯在下面展示 get() -> setInitialValue() return setInitialValue(); }

      get() -> getMap(Thread t)方法:

      ThreadLocalMap getMap(Thread t) { // 獲取當選線程的ThreadLocalMap return t.threadLocals; }

      get() -> setInitialValue()方法:

      /** * 這個方法于set方法邏輯一致,只是初始化的value為null */ private T setInitialValue() { // initialValue()返回null T value = initialValue(); // 后續操作與set()方法是完全相同的 // 這個方法是私有的無法被子類重寫 -> 相當于set()方法的一個副本,子類重寫了set()方法,還可以使用這個方法來初始化 Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; }

      4、ThreadLocalMap源碼分析

      ThreadLocalMap是整篇文章的重點,ThreadLocalMap是ThreadLocal的內部類,它提供了真正數據存取的能力;ThreadLocalMap為每個Thread都維護了一個table,這個table中的每一個Entry代表一個ThreadLocal(注意一個線程可以定義多個ThreadLocal,此時它們會存儲在table中不同的下標位置)和vlaue的組合。接下來通過源碼一層層的分析ThreadLocalMap的原理及實現。

      Entry是ThreadLocalMap的靜態內部類,它是一個負責元素存儲的key-value鍵值對數據結構,key是ThreadLocal,value是ThreadLocal傳入的相關的值。這里有一個重點知識,Entry繼承了WeakReference,所以很明顯的看出ThreadLocal k將會是一個弱引用,弱引用容易被JVM垃圾收集器回收,因此可能導致內存泄露的問題(后續在詳細分析,這里的重點是ThreadLocalMap的實現)。

      static class Entry extends WeakReference> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal k, Object v) { // key -> 是弱引用 super(k); // 保存值 value = v; } }

      在ThreadLocal中3.3 set()方法源碼分析中留下來createMap(t, value)的疑問,在獲取線程的ThreadLocalMap為空時,通過調用createMap(t, value)方法對ThreadLocalMap進行了初始化。

      // ThreadLocal中set()方法調用的createMap方法 void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }

      ThreadLocalMap(ThreadLocal firstKey, Object firstValue)源碼:

      ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { // 實例化一個大小為16的Entry數組,賦值給Entry[] table table = new Entry[INITIAL_CAPACITY]; // 根據當前的ThreadLocal計算其在table中的數組下標,這里不懂看前面3.2 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); // 通過傳入的ThreadLocal和value值,構造一個Entry賦值給table的指定位置的值 table[i] = new Entry(firstKey, firstValue); // 記錄table中Entry的個數,也擁有觸發擴容,初始化時為1 size = 1; // 設置擴容閾值len * 2 / 3 setThreshold(INITIAL_CAPACITY); }

      在ThreadLocal的set()方法中,當ThreadLocalMap不為空時,也就是說在上面4.2初始化之后,當前線程再次調用ThreadLocal的set()方法將會執行的是下面的邏輯。set()方法中有三個重點知識:

      ThreadLocal

      當計算的Entry下標位置不存在數據時,直接插入

      當存在數據時,通過線性探測來解決hash沖突

      當table中的Entry個數達到擴容閾值時,進行擴容處理

      private void set(ThreadLocal key, Object value) { Entry[] tab = table; int len = tab.length; // 計算數組下標 int i = key.threadLocalHashCode & (len-1); // 線性探測 // for循環中的內容就是從當前產生hash沖突的位置往后找 // 找到不為null的Entry 有兩種情況 1、key相等則更新 2、key=null則需要做replaceStaleEntry處理 // 如果為null,結束for循環 for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { // 獲取當前節點的key -> ThreadLocal 對象 ThreadLocal k = e.get(); // 如果key相同則直接替換,結束循環 if (k == key) { e.value = value; return; } // Entry存在,但是Entry的key為空,表示引用被垃圾回收器回收了 // 此時需要做比較復雜的處理,這個處理請看后面我的詳細分析,此處你可以理解為就是找個能放的索引位置放進去,然后結束循環 if (k == null) { replaceStaleEntry(key, value, i); return; } } // 在table[i] = null 的位置插入新的entry tab[i] = new Entry(key, value); // size + 1 int sz = ++size; // 如果沒有需求清理的key = null的entry,并且size到達擴容閾值 if (!cleanSomeSlots(i, sz) && sz >= threshold) // 擴容處理 rehash(); }

      set() -> nextIndex(i, len)方法:

      /** * 這里是線性探測的思想,一直往后遍歷 * 當到達數組的最后一個位置仍未找到滿足條件的,再從數組的最前面開始遍歷 */ private static int nextIndex(int i, int len) { // 當數組下標不越界的情況下 返回 i+1 否則返回 0 return ((i + 1 < len) ? i + 1 : 0); }

      set() -> replaceStaleEntry(key, value, i)方法:

      向前搜索,尋找其他同樣key為null被GC的Entry節點,并記錄下最后遍歷到的Entry索引,遍歷結束條件是Entry為null。這樣的好處是為了清理這些Entry的key被GC了的Entry節點。

      向后遍歷,ThreadLocal不同于hashmap,它是開放地址法,因此當前索引位置不一定就是這個Entry存放的位置,可能第一次存放的時候發生了hash碰撞,Entry的存儲發生了后移,因此要向后遍歷,尋找當前與Entry的key相等的槽。

      關于replaceStaleEntry(key, value, i)方法,我畫了一個簡圖,圖中并未包含所有場景,具體請詳細閱讀源碼(非常精彩的設計思路),假設進入這個方法時staleSlot = 8,并且key的hashcode = 0xx68

      源碼分析:

      private void replaceStaleEntry(ThreadLocal key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; // 將當前索引的值賦值給slotToExpunge,用于清理 int slotToExpunge = staleSlot; // 向前搜索,知道tab[i] == null // 如果tab[i] 不為空,但是tab[i]的key為空,也就是和當前節點一樣的情況,key被GC了,那么將當前索引下標的值賦值給slotToExpunge,記錄最小的索引值,后續從這里開始清理 for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e.get() == null) slotToExpunge = i; // 向后遍歷,直到tab[i]==null for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { // 獲取當前索引位置Entry的key ThreadLocal k = e.get(); // 如果key相等,證明當前這個節點后移到這里了,需要替換value // 替換的時候我們可以做一些優化,因為我們第一次命中的索引出存在Entry但是Entry的key被GC了,也就是說無法被訪問了,而我們這個節點是因為后移才存儲在這里,這個時候我們這個節點是不是可以重新放回去呢?放回去后下次不是一次就命中了么?就不需要往后遍歷尋找了么? if (k == key) { // 更新value e.value = value; // tab[i] 與 tab[staleSlot]交換位置 tab[i] = tab[staleSlot]; tab[staleSlot] = e; // 如果往前探索的第一個key=null的索引下標和當前替換回去的索引相同 // 由于做了交換,我們又能保證前面不存在key == null的節點了,那么只需將替換后的i的值賦值給slotToExpunge,這樣可以減少清理的循環次數 if (slotToExpunge == staleSlot) slotToExpunge = i; // 做清理工作和rehash cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } // 初始進來的時候我們有這句代碼 slotToExpunge == staleSlot // 所以如果slotToExpunge == staleSlot仍然成立,并且當前的key == null,那么我們就把當前的下標值賦值給slotToExpunge,很好理解還是為了縮小清理的范圍,大師們對提升性能總是那么極致 if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } // 執行到了這里,說明替換失敗了,沒找到要么就是它的key也被GC了,要么就是它是第一次set // 但是當前Entry的key是null,那我們就放這里吧,畢竟這個Entry也用不了 tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); // slotToExpunge != staleSlo表名需要清理key為null的Entry if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }

      關于replaceStaleEntry(ThreadLocal key, Object value, int staleSlot)往前探索并未發現滿足條件的Entry時,也就是代碼40行slotToExpunge == staleSlot滿足時,會做slotToExpunge = i操作,這個如果不清楚我做了圖來便于大家理解:

      expungeStaleEntry(int staleSlot)源碼分析:

      expungeStaleEntry(int staleSlot)主要做了三件事

      從staleSlot索引開始往后遍歷到第一個Entry節點不為空的下標這段區間中key=null的Entry節點清空處理

      在遍歷中如果key != null 需要做rehash處理,因為前面可能存在節點被清空了,重新根據k.threadLocalHashCode & (len - 1)計算索引,往后遍歷尋找第一個為null的Entry移動到這里

      返回i,這個i是從staleSlot往后遍歷到的第一個為null的Entry,這個值返回為了cleanSomeSlots(int i, int n),去清理后面的Entry,這里你可能會疑問為啥不直接用expungeStaleEntry(int staleSlot)方法直接全部遍歷一遍得了,但是你可以發現源碼這分塊的清理做了優化,具體實現請看后面的cleanSomeSlots(int i, int n)講解

      private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // 當前staleSlot索引處的Entry清空,注意不僅需要清空Entry還需要清空value,key本身已經為null了不需要再清空了 tab[staleSlot].value = null; tab[staleSlot] = null; size--; // 注意要及時的記錄table中Entry的個數 Entry e; int i; // 1、循環到第一個Entry不為空的位置 清空key == null的Entry和Entry的value for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal k = e.get(); // key == null 清空Entry和Entry的value if (k == null) { e.value = null; tab[i] = null; size--; // 注意要及時的記錄table中Entry的個數 } else { // 2、由于做了清空處理,我們要對Entry做rehash。因為他可能可以前移 int h = k.threadLocalHashCode & (len - 1); // 如果計算的h和當前的索引i不相等,嘗試從h開始往后尋找空的Entry if (h != i) { // 清空當前Entry tab[i] = null; // 循環找到第一個為空的Entry,并記錄它的索引 while (tab[h] != null) h = nextIndex(h, len); // tab[i]的值移到新的槽(可能是同一個) tab[h] = e; } } } // 3、返回i,這個i就是第一個為null的Entry return i; }

      cleanSomeSlots(int i, int n)源碼分析:

      cleanSomeSlots(int i, int n)也是對上面expungeStaleEntry(int staleSlot)方法中找到的第一個為null的Entry節點到table.legth的區間范圍內,Entry不為空但Entry的key為空的節點進行清理,這個清理不一定會進行到table的最后,因為它做了一個(n >>>= 1) != 0判斷,如果在n無符號右移1 == 0 時,并且這右移的期間沒有發現滿足清理的Entry那么就會結束往后尋找。

      n >>>=1 相當于 n= n>>>1,位運算右移一位相當于除以2

      舉個例子,如果i=5,n=16,此時如果在往后遍歷四次,也就是到i=9,仍然沒有滿足e != null && e.get() == null的Entry,那么后續10-16就不再遍歷了,這些都是對算法的優化。

      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]; // 找到滿足條件的做兩個操作 // 1、重置n // 2、調用expungeStaleEntry(i)清理 if (e != null && e.get() == null) { n = len; removed = true; i = expungeStaleEntry(i); } } while ( (n >>>= 1) != 0); // n = n >>> 1 相當于 除以2 return removed; }

      rehash()包含兩個部分的邏輯

      從table數組的第一個節點到最后一個節點中e != null && e.get() == null的Entry執行上面的expungeStaleEntry(int staleSlot)方法

      當達到擴容閾值,進行擴容處理

      private void rehash() { // 處理table中Entry的key被GC了的元素,后面將 expungeStaleEntries(); // 這里使用的雙倍閾值,也就是threshold在計算了一次threshold if (size >= threshold - threshold / 4) resize(); }

      expungeStaleEntries()源碼非常簡單,從table數組的第一個節點到最后一個節點中e != null && e.get() == null的Entry執行上面的expungeStaleEntry(int staleSlot)方法。

      private void expungeStaleEntries() { Entry[] tab = table; int len = tab.length; for (int j = 0; j < len; j++) { Entry e = tab[j]; // 如果e != null && e.get() == null 即 Entry的key被GC了 // 執行expungeStaleEntry(int staleSlot)方法 -> 上面詳細分析了 if (e != null && e.get() == null) expungeStaleEntry(j); } }

      resize()的源碼也比較簡單,主要做了三個操作:

      實例化一個原先大小兩倍的數組newTab

      遍歷原先的舊數組中的每一個節點,將不為空的Entry節點計算其在新數組中的下標,放入新的數組中,放入的方式與set一致,使用線性探測解決hash沖突,注意如果節點不為空,key為空,需要將節點和節點的value置為空,幫助GC

      設置新的擴容閾值,記錄新的size,替換table的引用

      private void resize() { Entry[] oldTab = table; int oldLen = oldTab.length; int newLen = oldLen * 2; Entry[] newTab = new Entry[newLen]; int count = 0; 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; }

      private Entry getEntry(ThreadLocal key) { // 計算數組下標 int i = key.threadLocalHashCode & (table.length - 1); // 取出Entry Entry e = table[i]; // 如果Entry不為空,且key相等直接返回 if (e != null && e.get() == key) return e; // 返回 else return getEntryAfterMiss(key, i, e); // 當前節點未命中 }

      getEntryAfterMiss(key, i, e)源碼分析:

      進入這個方法存在多種情況:

      節點發生了hash沖突,節點插入后移了(這種情況也有可能會被GC)

      節點為發送hash沖突,但是key被GC了

      private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal k = e.get(); // key相等則直接返回 if (k == key) return e; // key為空,要做清除和rehash if (k == null) expungeStaleEntry(i); // 往下遍歷直至末尾在從前開始 ((i + 1 < len) ? i + 1 : 0) else i = nextIndex(i, len); e = tab[i]; } // 可能未匹配上 return null; }

      5、ThreadLocal內存泄漏

      ThreadLocal內存泄漏是我們談及ThreadLocal存在的問題中所提及的最頻繁的一個,那么我們接下來就從為什么會內存泄漏和如何解決內存泄漏這兩個點來分析這個問題:

      當Thread中存在一個ThreadLocal的內存分布和引用情況的簡圖如下:

      我們知道Entry extends WeakReference>,也就是說ThreadLocal作為一個弱引用key,如果沒有被強引用所引用,那么它將活不過下次GC,這個也是上面產生那么多Entry的key為null的原因。當弱引用被指向的對象被GC那么將會導致我們程序員無法訪問到這個Entry中的value對象,再加上table中的Entry它不發生hash沖突或者擴容(這些方法中都會去處理這些key為null的Entry,java大佬們一直在優化這些問題),如果線程長期存活,那么這些key為null的Entry的value將永遠得不到GC,從而內存泄露。

      防止內存泄露的處理方式很簡單,ThreadLocal提供了remove()方法,供程序員主動清除Entry

      ThreadLocal的remove()方法:

      public void remove() { // 獲取當前線程的ThreadLocalMap ThreadLocalMap m = getMap(Thread.currentThread()); // 不為空則調用ThreadLocalMap的remove(ThreadLocal key)方法進行清理操作 if (m != null) m.remove(this); }

      ThreadLocalMap的remove(ThreadLocal key)方法:

      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(); // 對其他key為null的Entry做清理和不為null的節點做rehash expungeStaleEntry(i); return; } } }

      clear()方法源碼:

      public void clear() { // 引用置空 this.referent = null; }

      Java 任務調度

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。

      上一篇:wps文字超鏈接打開方式怎么改變,如何修改默認情況(WPS超鏈接設置)
      下一篇:Excel表格中怎樣將0值全部替換
      相關文章
      色天使亚洲综合一区二区| 亚洲VA成无码人在线观看天堂| 成人亚洲网站www在线观看| 夜色阁亚洲一区二区三区| 中日韩亚洲人成无码网站| 亚洲国产日韩在线一区| 亚洲人成影院午夜网站| 亚洲美女人黄网成人女| 亚洲黄色在线观看视频| 亚洲网站在线播放| 亚洲国产精品久久丫| 亚洲女人18毛片水真多| 亚洲av无码片区一区二区三区| 亚洲成aⅴ人片在线影院八| 亚洲免费中文字幕| 日韩亚洲不卡在线视频中文字幕在线观看 | 久久水蜜桃亚洲AV无码精品| 亚洲av日韩av永久无码电影| 久久久久久亚洲精品无码| 国产成人亚洲精品91专区高清| 亚洲AV无码乱码在线观看性色扶| 亚洲AV网站在线观看| 亚洲Av无码乱码在线观看性色| 亚洲伊人成无码综合网| 亚洲人精品午夜射精日韩| 久久精品国产精品亚洲色婷婷| 亚洲国语精品自产拍在线观看 | 亚洲av成人一区二区三区在线观看| 亚洲av无码不卡私人影院| 中国亚洲女人69内射少妇| 亚洲成AV人片一区二区密柚| 亚洲精品免费在线观看| 亚洲国产人成在线观看| 亚洲一区二区三区丝袜| 亚洲JIZZJIZZ中国少妇中文| 亚洲熟妇无码另类久久久| 亚洲自偷自拍另类12p| 亚洲91精品麻豆国产系列在线| 亚洲日韩AV无码一区二区三区人| 国产成人高清亚洲一区91| 亚洲熟女少妇一区二区|