<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??一站式輕松地調用各大LLM模型接口,支持GPT4、智譜、豆包、星火、月之暗面及文生圖、文生視頻 廣告
                > 本文來自一枝花算不算浪漫投稿, 原文地址:[https://juejin.cn/post/7199971894321119287](https://juejin.cn/post/7199971894321119287)。 [TOC] ### 前言 ![](https://raw.githubusercontent.com/Snailclimb/JavaGuide/master/docs/java/multi-thread/images/thread-local/1.png) **全文共10000+字,31張圖,這篇文章同樣耗費了不少的時間和精力才創作完成,原創不易,請大家點點關注+在看,感謝。** 對于`ThreadLocal`,大家的第一反應可能是很簡單呀,線程的變量副本,每個線程隔離。那這里有幾個問題大家可以思考一下: - `ThreadLocal`的key是**弱引用**,那么在 `ThreadLocal`.get()的時候,發生**GC**之后,key是否為**null**? - `ThreadLocal`中`ThreadLocalMap`的**數據結構**? - `ThreadLocalMap`的**Hash算法**? - `ThreadLocalMap`中**Hash沖突**如何解決? - `ThreadLocalMap`的**擴容機制**? - `ThreadLocalMap`中**過期key的清理機制**?**探測式清理**和**啟發式清理**流程? - `ThreadLocalMap.set()`方法實現原理? - `ThreadLocalMap.get()`方法實現原理? - 項目中`ThreadLocal`使用情況?遇到的坑? - ...... 上述的一些問題你是否都已經掌握的很清楚了呢?本文將圍繞這些問題使用圖文方式來剖析`ThreadLocal`的**點點滴滴**。 ### 目錄 **注明:** 本文源碼基于`JDK 1.8` ### `ThreadLocal`代碼演示 我們先看下`ThreadLocal`使用示例: ```java public class ThreadLocalTest { private List<String> messages = Lists.newArrayList(); public static final ThreadLocal<ThreadLocalTest> holder = ThreadLocal.withInitial(ThreadLocalTest::new); public static void add(String message) { holder.get().messages.add(message); } public static List<String> clear() { List<String> messages = holder.get().messages; holder.remove(); System.out.println("size: " + holder.get().messages.size()); return messages; } public static void main(String[] args) { ThreadLocalTest.add("一枝花算不算浪漫"); System.out.println(holder.get().messages); ThreadLocalTest.clear(); } } ``` 打印結果: ```java [一枝花算不算浪漫] size: 0 ``` `ThreadLocal`對象可以提供線程局部變量,每個線程`Thread`擁有一份自己的**副本變量**,多個線程互不干擾。 ### `ThreadLocal`的數據結構 ![](https://raw.githubusercontent.com/Snailclimb/JavaGuide/master/docs/java/multi-thread/images/thread-local/2.png) `Thread`類有一個類型為`ThreadLocal.ThreadLocalMap`的實例變量`threadLocals`,也就是說每個線程有一個自己的`ThreadLocalMap`。 `ThreadLocalMap`有自己的獨立實現,可以簡單地將它的`key`視作`ThreadLocal`,`value`為代碼中放入的值(實際上`key`并不是`ThreadLocal`本身,而是它的一個**弱引用**)。 每個線程在往`ThreadLocal`里放值的時候,都會往自己的`ThreadLocalMap`里存,讀也是以`ThreadLocal`作為引用,在自己的`map`里找對應的`key`,從而實現了**線程隔離**。 `ThreadLocalMap`有點類似`HashMap`的結構,只是`HashMap`是由**數組+鏈表**實現的,而`ThreadLocalMap`中并沒有**鏈表**結構。 我們還要注意`Entry`, 它的`key`是`ThreadLocal<?> k` ,繼承自`WeakReference`, 也就是我們常說的弱引用類型。 ### GC 之后key是否為null? 回應開頭的那個問題, `ThreadLocal` 的`key`是弱引用,那么在`ThreadLocal.get()`的時候,發生`GC`之后,`key`是否是`null`? 為了搞清楚這個問題,我們需要搞清楚`Java`的**四種引用類型**: - **強引用**:我們常常new出來的對象就是強引用類型,只要強引用存在,垃圾回收器將永遠不會回收被引用的對象,哪怕內存不足的時候 - **軟引用**:使用SoftReference修飾的對象被稱為軟引用,軟引用指向的對象在內存要溢出的時候被回收 - **弱引用**:使用WeakReference修飾的對象被稱為弱引用,只要發生垃圾回收,若這個對象只被弱引用指向,那么就會被回收 - **虛引用**:虛引用是最弱的引用,在 Java 中使用 PhantomReference 進行定義。虛引用中唯一的作用就是用隊列接收對象即將死亡的通知 接著再來看下代碼,我們使用反射的方式來看看`GC`后`ThreadLocal`中的數據情況:(下面代碼來源自:https://blog.csdn.net/thewindkee/article/details/103726942,本地運行演示GC回收場景) ```java public class ThreadLocalDemo { public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException { Thread t = new Thread(()->test("abc",false)); t.start(); t.join(); System.out.println("--gc后--"); Thread t2 = new Thread(() -> test("def", true)); t2.start(); t2.join(); } private static void test(String s,boolean isGC) { try { new ThreadLocal<>().set(s); if (isGC) { System.gc(); } Thread t = Thread.currentThread(); Class<? extends Thread> clz = t.getClass(); Field field = clz.getDeclaredField("threadLocals"); field.setAccessible(true); Object ThreadLocalMap = field.get(t); Class<?> tlmClass = ThreadLocalMap.getClass(); Field tableField = tlmClass.getDeclaredField("table"); tableField.setAccessible(true); Object[] arr = (Object[]) tableField.get(ThreadLocalMap); for (Object o : arr) { if (o != null) { Class<?> entryClass = o.getClass(); Field valueField = entryClass.getDeclaredField("value"); Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent"); valueField.setAccessible(true); referenceField.setAccessible(true); System.out.println(String.format("弱引用key:%s,值:%s", referenceField.get(o), valueField.get(o))); } } } catch (Exception e) { e.printStackTrace(); } } } ``` 結果如下: ```java 弱引用key:java.lang.ThreadLocal@433619b6,值:abc 弱引用key:java.lang.ThreadLocal@418a15e3,值:java.lang.ref.SoftReference@bf97a12 --gc后-- 弱引用key:null,值:def ``` ![](https://raw.githubusercontent.com/Snailclimb/JavaGuide/master/docs/java/multi-thread/images/thread-local/3.png) 如圖所示,因為這里創建的`ThreadLocal`并沒有指向任何值,也就是沒有任何引用: ```java new ThreadLocal<>().set(s); ``` 所以這里在`GC`之后,`key`就會被回收,我們看到上面`debug`中的`referent=null`, 如果**改動一下代碼:** ![](https://raw.githubusercontent.com/Snailclimb/JavaGuide/master/docs/java/multi-thread/images/thread-local/4.png) 這個問題剛開始看,如果沒有過多思考,**弱引用**,還有**垃圾回收**,那么肯定會覺得是`null`。 其實是不對的,因為題目說的是在做 `ThreadLocal.get()` 操作,證明其實還是有**強引用**存在的,所以 `key` 并不為 `null`,如下圖所示,`ThreadLocal`的**強引用**仍然是存在的。 ![image.png](https://raw.githubusercontent.com/Snailclimb/JavaGuide/master/docs/java/multi-thread/images/thread-local/5.png) 如果我們的**強引用**不存在的話,那么 `key` 就會被回收,也就是會出現我們 `value` 沒被回收,`key` 被回收,導致 `value` 永遠存在,出現內存泄漏。 ### `ThreadLocal.set()`方法源碼詳解 ![](https://raw.githubusercontent.com/Snailclimb/JavaGuide/master/docs/java/multi-thread/images/thread-local/6.png) `ThreadLocal`中的`set`方法原理如上圖所示,很簡單,主要是判斷`ThreadLocalMap`是否存在,然后使用`ThreadLocal`中的`set`方法進行數據處理。 代碼如下: ```java public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } void createMap(Thread t, T firstValue) { t.threadLocals = new `ThreadLocalMap`(this, firstValue); } ``` 主要的核心邏輯還是在`ThreadLocalMap`中的,一步步往下看,后面還有更詳細的剖析。 ### `ThreadLocalMap` Hash算法 既然是`Map`結構,那么`ThreadLocalMap`當然也要實現自己的`hash`算法來解決散列表數組沖突問題。 ```java int i = key.threadLocalHashCode & (len-1); ``` `ThreadLocalMap`中`hash`算法很簡單,這里`i`就是當前key在散列表中對應的數組下標位置。 這里最關鍵的就是`threadLocalHashCode`值的計算,`ThreadLocal`中有一個屬性為`HASH_INCREMENT = 0x61c88647` ```java public class ThreadLocal<T> { private final int threadLocalHashCode = nextHashCode(); private static AtomicInteger nextHashCode = new AtomicInteger(); private static final int HASH_INCREMENT = 0x61c88647; private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } static class ThreadLocalMap { 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); } } } ``` 每當創建一個`ThreadLocal`對象,這個`ThreadLocal.nextHashCode` 這個值就會增長 `0x61c88647` 。 這個值很特殊,它是**斐波那契數** 也叫 **黃金分割數**。`hash`增量為 這個數字,帶來的好處就是 `hash` **分布非常均勻**。 我們自己可以嘗試下: ![](https://raw.githubusercontent.com/Snailclimb/JavaGuide/master/docs/java/multi-thread/images/thread-local/8.png) 可以看到產生的哈希碼分布很均勻,這里不去細糾**斐波那契**具體算法,感興趣的可以自行查閱相關資料。 ### `ThreadLocalMap` Hash沖突 > **注明:** 下面所有示例圖中,**綠色塊**`Entry`代表**正常數據**,**灰色塊**代表`Entry`的`key`值為`null`,**已被垃圾回收**。**白色塊**表示`Entry`為`null`。 雖然`ThreadLocalMap`中使用了**黃金分割數來**作為`hash`計算因子,大大減少了`Hash`沖突的概率,但是仍然會存在沖突。 `HashMap`中解決沖突的方法是在數組上構造一個**鏈表**結構,沖突的數據掛載到鏈表上,如果鏈表長度超過一定數量則會轉化成**紅黑樹**。 而`ThreadLocalMap`中并沒有鏈表結構,所以這里不能適用`HashMap`解決沖突的方式了。 ![](https://raw.githubusercontent.com/Snailclimb/JavaGuide/master/docs/java/multi-thread/images/thread-local/7.png) 如上圖所示,如果我們插入一個`value=27`的數據,通過`hash`計算后應該落入第4個槽位中,而槽位4已經有了`Entry`數據。 此時就會線性向后查找,一直找到`Entry`為`null`的槽位才會停止查找,將當前元素放入此槽位中。當然迭代過程中還有其他的情況,比如遇到了`Entry`不為`null`且`key`值相等的情況,還有`Entry`中的`key`值為`null`的情況等等都會有不同的處理,后面會一一詳細講解。 這里還畫了一個`Entry`中的`key`為`null`的數據(**Entry=2的灰色塊數據**),因為`key`值是**弱引用**類型,所以會有這種數據存在。在`set`過程中,如果遇到了`key`過期的`Entry`數據,實際上是會進行一輪**探測式清理**操作的,具體操作方式后面會講到。 ### `ThreadLocalMap.set()`詳解 #### `ThreadLocalMap.set()`原理圖解 看完了`ThreadLocal` **hash算法**后,我們再來看`set`是如何實現的。 往`ThreadLocalMap`中`set`數據(**新增**或者**更新**數據)分為好幾種情況,針對不同的情況我們畫圖來說說明。 **第一種情況:** 通過`hash`計算后的槽位對應的`Entry`數據為空: ![](https://raw.githubusercontent.com/Snailclimb/JavaGuide/master/docs/java/multi-thread/images/thread-local/9.png) 這里直接將數據放到該槽位即可。 **第二種情況:** 槽位數據不為空,`key`值與當前`ThreadLocal`通過`hash`計算獲取的`key`值一致: ![](https://raw.githubusercontent.com/Snailclimb/JavaGuide/master/docs/java/multi-thread/images/thread-local/10.png) 這里直接更新該槽位的數據。 **第三種情況:** 槽位數據不為空,往后遍歷過程中,在找到`Entry`為`null`的槽位之前,沒有遇到`key`過期的`Entry`: ![](https://raw.githubusercontent.com/Snailclimb/JavaGuide/master/docs/java/multi-thread/images/thread-local/11.png) 遍歷散列數組,線性往后查找,如果找到`Entry`為`null`的槽位,則將數據放入該槽位中,或者往后遍歷過程中,遇到了**key值相等**的數據,直接更新即可。 **第四種情況:** 槽位數據不為空,往后遍歷過程中,在找到`Entry`為`null`的槽位之前,遇到`key`過期的`Entry`,如下圖,往后遍歷過程中,一到了`index=7`的槽位數據`Entry`的`key=null`: ![](https://raw.githubusercontent.com/Snailclimb/JavaGuide/master/docs/java/multi-thread/images/thread-local/12.png) 散列數組下標為7位置對應的`Entry`數據`key`為`null`,表明此數據`key`值已經被垃圾回收掉了,此時就會執行`replaceStaleEntry()`方法,該方法含義是**替換過期數據的邏輯**,以**index=7**位起點開始遍歷,進行探測式數據清理工作。 初始化探測式清理過期數據掃描的開始位置:`slotToExpunge = staleSlot = 7` 以當前`staleSlot`開始 向前迭代查找,找其他過期的數據,然后更新過期數據起始掃描下標`slotToExpunge`。`for`循環迭代,直到碰到`Entry`為`null`結束。 如果找到了過期的數據,繼續向前迭代,直到遇到`Entry=null`的槽位才停止迭代,如下圖所示,**slotToExpunge被更新為0**: ![](https://raw.githubusercontent.com/Snailclimb/JavaGuide/master/docs/java/multi-thread/images/thread-local/13.png) 以當前節點(`index=7`)向前迭代,檢測是否有過期的`Entry`數據,如果有則更新`slotToExpunge`值。碰到`null`則結束探測。以上圖為例`slotToExpunge`被更新為0。 上面向前迭代的操作是為了更新探測清理過期數據的起始下標`slotToExpunge`的值,這個值在后面會講解,它是用來判斷當前過期槽位`staleSlot`之前是否還有過期元素。 接著開始以`staleSlot`位置(index=7)向后迭代,**如果找到了相同key值的Entry數據:** ![](https://raw.githubusercontent.com/Snailclimb/JavaGuide/master/docs/java/multi-thread/images/thread-local/14.png) 從當前節點`staleSlot`向后查找`key`值相等的`Entry`元素,找到后更新`Entry`的值并交換`staleSlot`元素的位置(`staleSlot`位置為過期元素),更新`Entry`數據,然后開始進行過期`Entry`的清理工作,如下圖所示: ![Yu4oWT.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3ba9af057e1e?w=1336&h=361&f=png&s=63049) **向后遍歷過程中,如果沒有找到相同key值的Entry數據:** ![](https://raw.githubusercontent.com/Snailclimb/JavaGuide/master/docs/java/multi-thread/images/thread-local/15.png) 從當前節點`staleSlot`向后查找`key`值相等的`Entry`元素,直到`Entry`為`null`則停止尋找。通過上圖可知,此時`table`中沒有`key`值相同的`Entry`。 創建新的`Entry`,替換`table[stableSlot]`位置: ![](https://raw.githubusercontent.com/Snailclimb/JavaGuide/master/docs/java/multi-thread/images/thread-local/16.png) 替換完成后也是進行過期元素清理工作,清理工作主要是有兩個方法:`expungeStaleEntry()`和`cleanSomeSlots()`,具體細節后面會講到,請繼續往后看。 #### `ThreadLocalMap.set()`源碼詳解 上面已經用圖的方式解析了`set()`實現的原理,其實已經很清晰了,我們接著再看下源碼: `java.lang.ThreadLocal`.`ThreadLocalMap.set()`: ```java private void set(ThreadLocal<?> key, Object value) { 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)]) { `ThreadLocal`<?> k = e.get(); if (k == key) { e.value = value; return; } if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); } ``` 這里會通過`key`來計算在散列表中的對應位置,然后以當前`key`對應的桶的位置向后查找,找到可以使用的桶。 ```java Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); ``` 什么情況下桶才是可以使用的呢? 1. `k = key` 說明是替換操作,可以使用 2. 碰到一個過期的桶,執行替換邏輯,占用過期桶 3. 查找過程中,碰到桶中`Entry=null`的情況,直接使用 接著就是執行`for`循環遍歷,向后查找,我們先看下`nextIndex()`、`prevIndex()`方法實現: ![](https://raw.githubusercontent.com/Snailclimb/JavaGuide/master/docs/java/multi-thread/images/thread-local/17.png) ```java private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); } private static int prevIndex(int i, int len) { return ((i - 1 >= 0) ? i - 1 : len - 1); } ``` 接著看剩下`for`循環中的邏輯: 1. 遍歷當前`key`值對應的桶中`Entry`數據為空,這說明散列數組這里沒有數據沖突,跳出`for`循環,直接`set`數據到對應的桶中 2. 如果`key`值對應的桶中`Entry`數據不為空 2.1 如果`k = key`,說明當前`set`操作是一個替換操作,做替換邏輯,直接返回 2.2 如果`key = null`,說明當前桶位置的`Entry`是過期數據,執行`replaceStaleEntry()`方法(核心方法),然后返回 3. `for`循環執行完畢,繼續往下執行說明向后迭代的過程中遇到了`entry`為`null`的情況 3.1 在`Entry`為`null`的桶中創建一個新的`Entry`對象 3.2 執行`++size`操作 4. 調用`cleanSomeSlots()`做一次啟發式清理工作,清理散列數組中`Entry`的`key`過期的數據 4.1 如果清理工作完成后,未清理到任何數據,且`size`超過了閾值(數組長度的2/3),進行`rehash()`操作 4.2 `rehash()`中會先進行一輪探測式清理,清理過期`key`,清理完成后如果**size >= threshold - threshold / 4**,就會執行真正的擴容邏輯(擴容邏輯往后看) 接著重點看下`replaceStaleEntry()`方法,`replaceStaleEntry()`方法提供替換過期數據的功能,我們可以對應上面**第四種情況**的原理圖來再回顧下,具體代碼如下: `java.lang.ThreadLocal.ThreadLocalMap.replaceStaleEntry()`: ```java private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; int slotToExpunge = staleSlot; for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e.get() == null) slotToExpunge = i; 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; if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); } ``` `slotToExpunge`表示開始探測式清理過期數據的開始下標,默認從當前的`staleSlot`開始。以當前的`staleSlot`開始,向前迭代查找,找到沒有過期的數據,`for`循環一直碰到`Entry`為`null`才會結束。如果向前找到了過期數據,更新探測清理過期數據的開始下標為i,即`slotToExpunge=i` ```java for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)){ if (e.get() == null){ slotToExpunge = i; } } ``` 接著開始從`staleSlot`向后查找,也是碰到`Entry`為`null`的桶結束。 如果迭代過程中,**碰到k == key**,這說明這里是替換邏輯,替換新數據并且交換當前`staleSlot`位置。如果`slotToExpunge == staleSlot`,這說明`replaceStaleEntry()`一開始向前查找過期數據時并未找到過期的`Entry`數據,接著向后查找過程中也未發現過期數據,修改開始探測式清理過期數據的下標為當前循環的index,即`slotToExpunge = i`。最后調用`cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);`進行啟發式過期數據清理。 ```java if (k == key) { e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } ``` `cleanSomeSlots()`和`expungeStaleEntry()`方法后面都會細講,這兩個是和清理相關的方法,一個是過期`key`相關`Entry`的啟發式清理(`Heuristically scan`),另一個是過期`key`相關`Entry`的探測式清理。 **如果k != key**則會接著往下走,`k == null`說明當前遍歷的`Entry`是一個過期數據,`slotToExpunge == staleSlot`說明,一開始的向前查找數據并未找到過期的`Entry`。如果條件成立,則更新`slotToExpunge` 為當前位置,這個前提是前驅節點掃描時未發現過期數據。 ```java if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; ``` 往后迭代的過程中如果沒有找到`k == key`的數據,且碰到`Entry`為`null`的數據,則結束當前的迭代操作。此時說明這里是一個添加的邏輯,將新的數據添加到`table[staleSlot]` 對應的`slot`中。 ```java tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); ``` 最后判斷除了`staleSlot`以外,還發現了其他過期的`slot`數據,就要開啟清理數據的邏輯: ```java if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); ``` ### `ThreadLocalMap`過期key的探測式清理流程 上面我們有提及`ThreadLocalMap`的兩種過期`key`數據清理方式:**探測式清理**和**啟發式清理**。 我們先講下探測式清理,也就是`expungeStaleEntry`方法,遍歷散列數組,從開始位置向后探測清理過期數據,將過期數據的`Entry`設置為`null`,沿途中碰到未過期的數據則將此數據`rehash`后重新在`table`數組中定位,如果定位的位置已經有了數據,則會將未過期的數據放到最靠近此位置的`Entry=null`的桶中,使`rehash`后的`Entry`數據距離正確的桶的位置更近一些。操作邏輯如下: ![](https://raw.githubusercontent.com/Snailclimb/JavaGuide/master/docs/java/multi-thread/images/thread-local/18.png) 如上圖,`set(27)` 經過hash計算后應該落到`index=4`的桶中,由于`index=4`桶已經有了數據,所以往后迭代最終數據放入到`index=7`的桶中,放入后一段時間后`index=5`中的`Entry`數據`key`變為了`null` ![](https://raw.githubusercontent.com/Snailclimb/JavaGuide/master/docs/java/multi-thread/images/thread-local/19.png) 如果再有其他數據`set`到`map`中,就會觸發**探測式清理**操作。 如上圖,執行**探測式清理**后,`index=5`的數據被清理掉,繼續往后迭代,到`index=7`的元素時,經過`rehash`后發現該元素正確的`index=4`,而此位置已經已經有了數據,往后查找離`index=4`最近的`Entry=null`的節點(剛被探測式清理掉的數據:index=5),找到后移動`index= 7`的數據到`index=5`中,此時桶的位置離正確的位置`index=4`更近了。 經過一輪探測式清理后,`key`過期的數據會被清理掉,沒過期的數據經過`rehash`重定位后所處的桶位置理論上更接近`i= key.hashCode & (tab.len - 1)`的位置。這種優化會提高整個散列表查詢性能。 接著看下`expungeStaleEntry()`具體流程,我們還是以先原理圖后源碼講解的方式來一步步梳理: ![](https://raw.githubusercontent.com/Snailclimb/JavaGuide/master/docs/java/multi-thread/images/thread-local/20.png) 我們假設`expungeStaleEntry(3)` 來調用此方法,如上圖所示,我們可以看到`ThreadLocalMap`中`table`的數據情況,接著執行清理操作: ![](https://raw.githubusercontent.com/Snailclimb/JavaGuide/master/docs/java/multi-thread/images/thread-local/21.png) 第一步是清空當前`staleSlot`位置的數據,`index=3`位置的`Entry`變成了`null`。然后接著往后探測: ![](https://raw.githubusercontent.com/Snailclimb/JavaGuide/master/docs/java/multi-thread/images/thread-local/22.png) 執行完第二步后,index=4的元素挪到index=3的槽位中。 繼續往后迭代檢查,碰到正常數據,計算該數據位置是否偏移,如果被偏移,則重新計算`slot`位置,目的是讓正常數據盡可能存放在正確位置或離正確位置更近的位置 ![](https://raw.githubusercontent.com/Snailclimb/JavaGuide/master/docs/java/multi-thread/images/thread-local/23.png) 在往后迭代的過程中碰到空的槽位,終止探測,這樣一輪探測式清理工作就完成了,接著我們繼續看看具體**實現源代碼**: ```java private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; tab[staleSlot].value = null; tab[staleSlot] = null; size--; Entry e; int i; for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { `ThreadLocal`<?> k = e.get(); if (k == null) { e.value = null; tab[i] = null; size--; } else { int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null; while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; } ``` 這里我們還是以`staleSlot=3` 來做示例說明,首先是將`tab[staleSlot]`槽位的數據清空,然后設置`size--` 接著以`staleSlot`位置往后迭代,如果遇到`k==null`的過期數據,也是清空該槽位數據,然后`size--` ```java ThreadLocal<?> k = e.get(); if (k == null) { e.value = null; tab[i] = null; size--; } ``` 如果`key`沒有過期,重新計算當前`key`的下標位置是不是當前槽位下標位置,如果不是,那么說明產生了`hash`沖突,此時以新計算出來正確的槽位位置往后迭代,找到最近一個可以存放`entry`的位置。 ```java int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null; while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } ``` 這里是處理正常的產生`Hash`沖突的數據,經過迭代后,有過`Hash`沖突數據的`Entry`位置會更靠近正確位置,這樣的話,查詢的時候 效率才會更高。 ### `ThreadLocalMap`擴容機制 在``ThreadLocalMap.set()`方法的最后,如果執行完啟發式清理工作后,未清理到任何數據,且當前散列數組中`Entry`的數量已經達到了列表的擴容閾值`(len*2/3)`,就開始執行`rehash()`邏輯: ```java if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); ``` 接著看下`rehash()`具體實現: ```java private void rehash() { expungeStaleEntries(); if (size >= threshold - threshold / 4) resize(); } 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); } } ``` 這里首先是會進行探測式清理工作,從`table`的起始位置往后清理,上面有分析清理的詳細流程。清理完成之后,`table`中可能有一些`key`為`null`的`Entry`數據被清理掉,所以此時通過判斷`size >= threshold - threshold / 4` 也就是`size >= threshold* 3/4` 來決定是否擴容。 我們還記得上面進行`rehash()`的閾值是`size >= threshold`,所以當面試官套路我們`ThreadLocalMap`擴容機制的時候 我們一定要說清楚這兩個步驟: ![](https://raw.githubusercontent.com/Snailclimb/JavaGuide/master/docs/java/multi-thread/images/thread-local/24.png) 接著看看具體的`resize()`方法,為了方便演示,我們以`oldTab.len=8`來舉例: ![](https://raw.githubusercontent.com/Snailclimb/JavaGuide/master/docs/java/multi-thread/images/thread-local/25.png) 擴容后的`tab`的大小為`oldLen * 2`,然后遍歷老的散列表,重新計算`hash`位置,然后放到新的`tab`數組中,如果出現`hash`沖突則往后尋找最近的`entry`為`null`的槽位,遍歷完成之后,`oldTab`中所有的`entry`數據都已經放入到新的`tab`中了。重新計算`tab`下次擴容的**閾值**,具體代碼如下: ```java 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; } 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; } ``` ### `ThreadLocalMap.get()`詳解 上面已經看完了`set()`方法的源碼,其中包括`set`數據、清理數據、優化數據桶的位置等操作,接著看看`get()`操作的原理。 #### `ThreadLocalMap.get()`圖解 **第一種情況:** 通過查找`key`值計算出散列表中`slot`位置,然后該`slot`位置中的`Entry.key`和查找的`key`一致,則直接返回: ![](https://raw.githubusercontent.com/Snailclimb/JavaGuide/master/docs/java/multi-thread/images/thread-local/26.png) **第二種情況:** `slot`位置中的`Entry.key`和要查找的`key`不一致: ![](https://raw.githubusercontent.com/Snailclimb/JavaGuide/master/docs/java/multi-thread/images/thread-local/27.png) 我們以`get(ThreadLocal1)`為例,通過`hash`計算后,正確的`slot`位置應該是4,而`index=4`的槽位已經有了數據,且`key`值不等于`ThreadLocal1`,所以需要繼續往后迭代查找。 迭代到`index=5`的數據時,此時`Entry.key=null`,觸發一次探測式數據回收操作,執行`expungeStaleEntry()`方法,執行完后,`index 5,8`的數據都會被回收,而`index 6,7`的數據都會前移,此時繼續往后迭代,到`index = 6`的時候即找到了`key`值相等的`Entry`數據,如下圖所示: ![](https://raw.githubusercontent.com/Snailclimb/JavaGuide/master/docs/java/multi-thread/images/thread-local/28.png) #### `ThreadLocalMap.get()`源碼詳解 `java.lang.ThreadLocal.ThreadLocalMap.getEntry()`: ```java 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); } 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; if (k == null) expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; } ``` ### `ThreadLocalMap`過期key的啟發式清理流程 上面多次提及到`ThreadLocalMap`過期可以的兩種清理方式:**探測式清理(expungeStaleEntry())**、**啟發式清理(cleanSomeSlots())** 探測式清理是以當前`Entry` 往后清理,遇到值為`null`則結束清理,屬于**線性探測清理**。 而啟發式清理被作者定義為:**Heuristically scan some cells looking for stale entries**. ![](https://raw.githubusercontent.com/Snailclimb/JavaGuide/master/docs/java/multi-thread/images/thread-local/29.png) 具體代碼如下: ```java 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]; if (e != null && e.get() == null) { n = len; removed = true; i = expungeStaleEntry(i); } } while ( (n >>>= 1) != 0); return removed; } ``` ### `InheritableThreadLocal` 我們使用`ThreadLocal`的時候,在異步場景下是無法給子線程共享父線程中創建的線程副本數據的。 為了解決這個問題,JDK中還有一個`InheritableThreadLocal`類,我們來看一個例子: ```java public class InheritableThreadLocalDemo { public static void main(String[] args) { ThreadLocal<String> ThreadLocal = new ThreadLocal<>(); ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>(); ThreadLocal.set("父類數據:threadLocal"); inheritableThreadLocal.set("父類數據:inheritableThreadLocal"); new Thread(new Runnable() { @Override public void run() { System.out.println("子線程獲取父類`ThreadLocal`數據:" + `ThreadLocal`.get()); System.out.println("子線程獲取父類inheritableThreadLocal數據:" + inheritableThreadLocal.get()); } }).start(); } } ``` 打印結果: ```java 子線程獲取父類`ThreadLocal`數據:null 子線程獲取父類inheritableThreadLocal數據:父類數據:inheritableThreadLocal ``` 實現原理是子線程是通過在父線程中通過調用`new Thread()`方法來創建子線程,`Thread#init`方法在`Thread`的構造方法中被調用。在`init`方法中拷貝父線程數據到子線程中: ```java private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { if (name == null) { throw new NullPointerException("name cannot be null"); } if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); this.stackSize = stackSize; tid = nextThreadID(); } ``` 但`InheritableThreadLocal`仍然有缺陷,一般我們做異步化處理都是使用的線程池,而`InheritableThreadLocal`是在`new Thread`中的`init()`方法給賦值的,而線程池是線程復用的邏輯,所以這里會存在問題。 當然,有問題出現就會有解決問題的方案,阿里巴巴開源了一個`TransmittableThreadLocal`組件就可以解決這個問題,這里就不再延伸,感興趣的可自行查閱資料。 ### `ThreadLocal`項目中使用實戰 #### `ThreadLocal`使用場景 我們現在項目中日志記錄用的是`ELK+Logstash`,最后在`Kibana`中進行展示和檢索。 現在都是分布式系統統一對外提供服務,項目間調用的關系可以通過traceId來關聯,但是不同項目之間如何傳遞`traceId`呢? 這里我們使用`org.slf4j.MDC`來實現此功能,內部就是通過`ThreadLocal`來實現的,具體實現如下: 當前端發送請求到**服務A**時,**服務A**會生成一個類似`UUID`的`traceId`字符串,將此字符串放入當前線程的`ThreadLocal`中,在調用**服務B**的時候,將`traceId`寫入到請求的`Header`中,**服務B**在接收請求時會先判斷請求的`Header`中是否有`traceId`,如果存在則寫入自己線程的`ThreadLocal`中。 ![](https://raw.githubusercontent.com/Snailclimb/JavaGuide/master/docs/java/multi-thread/images/thread-local/30.png) 圖中的`requestId`即為我們各個系統鏈路關聯的`traceId`,系統間互相調用,通過這個`requestId`即可找到對應鏈路,這里還有會有一些其他場景: ![](https://raw.githubusercontent.com/Snailclimb/JavaGuide/master/docs/java/multi-thread/images/thread-local/31.png) 針對于這些場景,我們都可以有相應的解決方案,如下所示 #### Feign遠程調用解決方案 **服務發送請求:** ```java @Component @Slf4j public class FeignInvokeInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { String requestId = MDC.get("requestId"); if (StringUtils.isNotBlank(requestId)) { template.header("requestId", requestId); } } } ``` **服務接收請求:** ```java @Slf4j @Component public class LogInterceptor extends HandlerInterceptorAdapter { @Override public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3) { MDC.remove("requestId"); } @Override public void postHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, ModelAndView arg3) { } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String requestId = request.getHeader(BaseConstant.REQUEST_ID_KEY); if (StringUtils.isBlank(requestId)) { requestId = UUID.randomUUID().toString().replace("-", ""); } MDC.put("requestId", requestId); return true; } } ``` #### 線程池異步調用,requestId傳遞 因為`MDC`是基于`ThreadLocal`去實現的,異步過程中,子線程并沒有辦法獲取到父線程`ThreadLocal`存儲的數據,所以這里可以自定義線程池執行器,修改其中的`run()`方法: ```java public class MyThreadPoolTaskExecutor extends ThreadPoolTaskExecutor { @Override public void execute(Runnable runnable) { Map<String, String> context = MDC.getCopyOfContextMap(); super.execute(() -> run(runnable, context)); } @Override private void run(Runnable runnable, Map<String, String> context) { if (context != null) { MDC.setContextMap(context); } try { runnable.run(); } finally { MDC.remove(); } } } ``` #### 使用MQ發送消息給第三方系統 在MQ發送的消息體中自定義屬性`requestId`,接收方消費消息后,自己解析`requestId`使用即可。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看