> 本文來自一枝花算不算浪漫投稿, 原文地址:[https://juejin.cn/post/7199971894321119287](https://juejin.cn/post/7199971894321119287)。
[TOC]
### 前言

**全文共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`的數據結構

`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
```

如圖所示,因為這里創建的`ThreadLocal`并沒有指向任何值,也就是沒有任何引用:
```java
new ThreadLocal<>().set(s);
```
所以這里在`GC`之后,`key`就會被回收,我們看到上面`debug`中的`referent=null`, 如果**改動一下代碼:**

這個問題剛開始看,如果沒有過多思考,**弱引用**,還有**垃圾回收**,那么肯定會覺得是`null`。
其實是不對的,因為題目說的是在做 `ThreadLocal.get()` 操作,證明其實還是有**強引用**存在的,所以 `key` 并不為 `null`,如下圖所示,`ThreadLocal`的**強引用**仍然是存在的。

如果我們的**強引用**不存在的話,那么 `key` 就會被回收,也就是會出現我們 `value` 沒被回收,`key` 被回收,導致 `value` 永遠存在,出現內存泄漏。
### `ThreadLocal.set()`方法源碼詳解

`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` **分布非常均勻**。
我們自己可以嘗試下:

可以看到產生的哈希碼分布很均勻,這里不去細糾**斐波那契**具體算法,感興趣的可以自行查閱相關資料。
### `ThreadLocalMap` Hash沖突
> **注明:** 下面所有示例圖中,**綠色塊**`Entry`代表**正常數據**,**灰色塊**代表`Entry`的`key`值為`null`,**已被垃圾回收**。**白色塊**表示`Entry`為`null`。
雖然`ThreadLocalMap`中使用了**黃金分割數來**作為`hash`計算因子,大大減少了`Hash`沖突的概率,但是仍然會存在沖突。
`HashMap`中解決沖突的方法是在數組上構造一個**鏈表**結構,沖突的數據掛載到鏈表上,如果鏈表長度超過一定數量則會轉化成**紅黑樹**。
而`ThreadLocalMap`中并沒有鏈表結構,所以這里不能適用`HashMap`解決沖突的方式了。

如上圖所示,如果我們插入一個`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`數據為空:

這里直接將數據放到該槽位即可。
**第二種情況:** 槽位數據不為空,`key`值與當前`ThreadLocal`通過`hash`計算獲取的`key`值一致:

這里直接更新該槽位的數據。
**第三種情況:** 槽位數據不為空,往后遍歷過程中,在找到`Entry`為`null`的槽位之前,沒有遇到`key`過期的`Entry`:

遍歷散列數組,線性往后查找,如果找到`Entry`為`null`的槽位,則將數據放入該槽位中,或者往后遍歷過程中,遇到了**key值相等**的數據,直接更新即可。
**第四種情況:** 槽位數據不為空,往后遍歷過程中,在找到`Entry`為`null`的槽位之前,遇到`key`過期的`Entry`,如下圖,往后遍歷過程中,一到了`index=7`的槽位數據`Entry`的`key=null`:

散列數組下標為7位置對應的`Entry`數據`key`為`null`,表明此數據`key`值已經被垃圾回收掉了,此時就會執行`replaceStaleEntry()`方法,該方法含義是**替換過期數據的邏輯**,以**index=7**位起點開始遍歷,進行探測式數據清理工作。
初始化探測式清理過期數據掃描的開始位置:`slotToExpunge = staleSlot = 7`
以當前`staleSlot`開始 向前迭代查找,找其他過期的數據,然后更新過期數據起始掃描下標`slotToExpunge`。`for`循環迭代,直到碰到`Entry`為`null`結束。
如果找到了過期的數據,繼續向前迭代,直到遇到`Entry=null`的槽位才停止迭代,如下圖所示,**slotToExpunge被更新為0**:

以當前節點(`index=7`)向前迭代,檢測是否有過期的`Entry`數據,如果有則更新`slotToExpunge`值。碰到`null`則結束探測。以上圖為例`slotToExpunge`被更新為0。
上面向前迭代的操作是為了更新探測清理過期數據的起始下標`slotToExpunge`的值,這個值在后面會講解,它是用來判斷當前過期槽位`staleSlot`之前是否還有過期元素。
接著開始以`staleSlot`位置(index=7)向后迭代,**如果找到了相同key值的Entry數據:**

從當前節點`staleSlot`向后查找`key`值相等的`Entry`元素,找到后更新`Entry`的值并交換`staleSlot`元素的位置(`staleSlot`位置為過期元素),更新`Entry`數據,然后開始進行過期`Entry`的清理工作,如下圖所示:

**向后遍歷過程中,如果沒有找到相同key值的Entry數據:**

從當前節點`staleSlot`向后查找`key`值相等的`Entry`元素,直到`Entry`為`null`則停止尋找。通過上圖可知,此時`table`中沒有`key`值相同的`Entry`。
創建新的`Entry`,替換`table[stableSlot]`位置:

替換完成后也是進行過期元素清理工作,清理工作主要是有兩個方法:`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()`方法實現:

```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`數據距離正確的桶的位置更近一些。操作邏輯如下:

如上圖,`set(27)` 經過hash計算后應該落到`index=4`的桶中,由于`index=4`桶已經有了數據,所以往后迭代最終數據放入到`index=7`的桶中,放入后一段時間后`index=5`中的`Entry`數據`key`變為了`null`

如果再有其他數據`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()`具體流程,我們還是以先原理圖后源碼講解的方式來一步步梳理:

我們假設`expungeStaleEntry(3)` 來調用此方法,如上圖所示,我們可以看到`ThreadLocalMap`中`table`的數據情況,接著執行清理操作:

第一步是清空當前`staleSlot`位置的數據,`index=3`位置的`Entry`變成了`null`。然后接著往后探測:

執行完第二步后,index=4的元素挪到index=3的槽位中。
繼續往后迭代檢查,碰到正常數據,計算該數據位置是否偏移,如果被偏移,則重新計算`slot`位置,目的是讓正常數據盡可能存放在正確位置或離正確位置更近的位置

在往后迭代的過程中碰到空的槽位,終止探測,這樣一輪探測式清理工作就完成了,接著我們繼續看看具體**實現源代碼**:
```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`擴容機制的時候 我們一定要說清楚這兩個步驟:

接著看看具體的`resize()`方法,為了方便演示,我們以`oldTab.len=8`來舉例:

擴容后的`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`一致,則直接返回:

**第二種情況:** `slot`位置中的`Entry.key`和要查找的`key`不一致:

我們以`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`數據,如下圖所示:

#### `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**.

具體代碼如下:
```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`中。

圖中的`requestId`即為我們各個系統鏈路關聯的`traceId`,系統間互相調用,通過這個`requestId`即可找到對應鏈路,這里還有會有一些其他場景:

針對于這些場景,我們都可以有相應的解決方案,如下所示
#### 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`使用即可。
- 一.JVM
- 1.1 java代碼是怎么運行的
- 1.2 JVM的內存區域
- 1.3 JVM運行時內存
- 1.4 JVM內存分配策略
- 1.5 JVM類加載機制與對象的生命周期
- 1.6 常用的垃圾回收算法
- 1.7 JVM垃圾收集器
- 1.8 CMS垃圾收集器
- 1.9 G1垃圾收集器
- 2.面試相關文章
- 2.1 可能是把Java內存區域講得最清楚的一篇文章
- 2.0 GC調優參數
- 2.1GC排查系列
- 2.2 內存泄漏和內存溢出
- 2.2.3 深入理解JVM-hotspot虛擬機對象探秘
- 1.10 并發的可達性分析相關問題
- 二.Java集合架構
- 1.ArrayList深入源碼分析
- 2.Vector深入源碼分析
- 3.LinkedList深入源碼分析
- 4.HashMap深入源碼分析
- 5.ConcurrentHashMap深入源碼分析
- 6.HashSet,LinkedHashSet 和 LinkedHashMap
- 7.容器中的設計模式
- 8.集合架構之面試指南
- 9.TreeSet和TreeMap
- 三.Java基礎
- 1.基礎概念
- 1.1 Java程序初始化的順序是怎么樣的
- 1.2 Java和C++的區別
- 1.3 反射
- 1.4 注解
- 1.5 泛型
- 1.6 字節與字符的區別以及訪問修飾符
- 1.7 深拷貝與淺拷貝
- 1.8 字符串常量池
- 2.面向對象
- 3.關鍵字
- 4.基本數據類型與運算
- 5.字符串與數組
- 6.異常處理
- 7.Object 通用方法
- 8.Java8
- 8.1 Java 8 Tutorial
- 8.2 Java 8 數據流(Stream)
- 8.3 Java 8 并發教程:線程和執行器
- 8.4 Java 8 并發教程:同步和鎖
- 8.5 Java 8 并發教程:原子變量和 ConcurrentMap
- 8.6 Java 8 API 示例:字符串、數值、算術和文件
- 8.7 在 Java 8 中避免 Null 檢查
- 8.8 使用 Intellij IDEA 解決 Java 8 的數據流問題
- 四.Java 并發編程
- 1.線程的實現/創建
- 2.線程生命周期/狀態轉換
- 3.線程池
- 4.線程中的協作、中斷
- 5.Java鎖
- 5.1 樂觀鎖、悲觀鎖和自旋鎖
- 5.2 Synchronized
- 5.3 ReentrantLock
- 5.4 公平鎖和非公平鎖
- 5.3.1 說說ReentrantLock的實現原理,以及ReentrantLock的核心源碼是如何實現的?
- 5.5 鎖優化和升級
- 6.多線程的上下文切換
- 7.死鎖的產生和解決
- 8.J.U.C(java.util.concurrent)
- 0.簡化版(快速復習用)
- 9.鎖優化
- 10.Java 內存模型(JMM)
- 11.ThreadLocal詳解
- 12 CAS
- 13.AQS
- 0.ArrayBlockingQueue和LinkedBlockingQueue的實現原理
- 1.DelayQueue的實現原理
- 14.Thread.join()實現原理
- 15.PriorityQueue 的特性和原理
- 16.CyclicBarrier的實際使用場景
- 五.Java I/O NIO
- 1.I/O模型簡述
- 2.Java NIO之緩沖區
- 3.JAVA NIO之文件通道
- 4.Java NIO之套接字通道
- 5.Java NIO之選擇器
- 6.基于 Java NIO 實現簡單的 HTTP 服務器
- 7.BIO-NIO-AIO
- 8.netty(一)
- 9.NIO面試題
- 六.Java設計模式
- 1.單例模式
- 2.策略模式
- 3.模板方法
- 4.適配器模式
- 5.簡單工廠
- 6.門面模式
- 7.代理模式
- 七.數據結構和算法
- 1.什么是紅黑樹
- 2.二叉樹
- 2.1 二叉樹的前序、中序、后序遍歷
- 3.排序算法匯總
- 4.java實現鏈表及鏈表的重用操作
- 4.1算法題-鏈表反轉
- 5.圖的概述
- 6.常見的幾道字符串算法題
- 7.幾道常見的鏈表算法題
- 8.leetcode常見算法題1
- 9.LRU緩存策略
- 10.二進制及位運算
- 10.1.二進制和十進制轉換
- 10.2.位運算
- 11.常見鏈表算法題
- 12.算法好文推薦
- 13.跳表
- 八.Spring 全家桶
- 1.Spring IOC
- 2.Spring AOP
- 3.Spring 事務管理
- 4.SpringMVC 運行流程和手動實現
- 0.Spring 核心技術
- 5.spring如何解決循環依賴問題
- 6.springboot自動裝配原理
- 7.Spring中的循環依賴解決機制中,為什么要三級緩存,用二級緩存不夠嗎
- 8.beanFactory和factoryBean有什么區別
- 九.數據庫
- 1.mybatis
- 1.1 MyBatis-# 與 $ 區別以及 sql 預編譯
- Mybatis系列1-Configuration
- Mybatis系列2-SQL執行過程
- Mybatis系列3-之SqlSession
- Mybatis系列4-之Executor
- Mybatis系列5-StatementHandler
- Mybatis系列6-MappedStatement
- Mybatis系列7-參數設置揭秘(ParameterHandler)
- Mybatis系列8-緩存機制
- 2.淺談聚簇索引和非聚簇索引的區別
- 3.mysql 證明為什么用limit時,offset很大會影響性能
- 4.MySQL中的索引
- 5.數據庫索引2
- 6.面試題收集
- 7.MySQL行鎖、表鎖、間隙鎖詳解
- 8.數據庫MVCC詳解
- 9.一條SQL查詢語句是如何執行的
- 10.MySQL 的 crash-safe 原理解析
- 11.MySQL 性能優化神器 Explain 使用分析
- 12.mysql中,一條update語句執行的過程是怎么樣的?期間用到了mysql的哪些log,分別有什么作用
- 十.Redis
- 0.快速復習回顧Redis
- 1.通俗易懂的Redis數據結構基礎教程
- 2.分布式鎖(一)
- 3.分布式鎖(二)
- 4.延時隊列
- 5.位圖Bitmaps
- 6.Bitmaps(位圖)的使用
- 7.Scan
- 8.redis緩存雪崩、緩存擊穿、緩存穿透
- 9.Redis為什么是單線程、及高并發快的3大原因詳解
- 10.布隆過濾器你值得擁有的開發利器
- 11.Redis哨兵、復制、集群的設計原理與區別
- 12.redis的IO多路復用
- 13.相關redis面試題
- 14.redis集群
- 十一.中間件
- 1.RabbitMQ
- 1.1 RabbitMQ實戰,hello world
- 1.2 RabbitMQ 實戰,工作隊列
- 1.3 RabbitMQ 實戰, 發布訂閱
- 1.4 RabbitMQ 實戰,路由
- 1.5 RabbitMQ 實戰,主題
- 1.6 Spring AMQP 的 AMQP 抽象
- 1.7 Spring AMQP 實戰 – 整合 RabbitMQ 發送郵件
- 1.8 RabbitMQ 的消息持久化與 Spring AMQP 的實現剖析
- 1.9 RabbitMQ必備核心知識
- 2.RocketMQ 的幾個簡單問題與答案
- 2.Kafka
- 2.1 kafka 基礎概念和術語
- 2.2 Kafka的重平衡(Rebalance)
- 2.3.kafka日志機制
- 2.4 kafka是pull還是push的方式傳遞消息的?
- 2.5 Kafka的數據處理流程
- 2.6 Kafka的腦裂預防和處理機制
- 2.7 Kafka中partition副本的Leader選舉機制
- 2.8 如果Leader掛了的時候,follower沒來得及同步,是否會出現數據不一致
- 2.9 kafka的partition副本是否會出現腦裂情況
- 十二.Zookeeper
- 0.什么是Zookeeper(漫畫)
- 1.使用docker安裝Zookeeper偽集群
- 3.ZooKeeper-Plus
- 4.zk實現分布式鎖
- 5.ZooKeeper之Watcher機制
- 6.Zookeeper之選舉及數據一致性
- 十三.計算機網絡
- 1.進制轉換:二進制、八進制、十六進制、十進制之間的轉換
- 2.位運算
- 3.計算機網絡面試題匯總1
- 十四.Docker
- 100.面試題收集合集
- 1.美團面試常見問題總結
- 2.b站部分面試題
- 3.比心面試題
- 4.騰訊面試題
- 5.哈羅部分面試
- 6.筆記
- 十五.Storm
- 1.Storm和流處理簡介
- 2.Storm 核心概念詳解
- 3.Storm 單機版本環境搭建
- 4.Storm 集群環境搭建
- 5.Storm 編程模型詳解
- 6.Storm 項目三種打包方式對比分析
- 7.Storm 集成 Redis 詳解
- 8.Storm 集成 HDFS 和 HBase
- 9.Storm 集成 Kafka
- 十六.Elasticsearch
- 1.初識ElasticSearch
- 2.文檔基本CRUD、集群健康檢查
- 3.shard&replica
- 4.document核心元數據解析及ES的并發控制
- 5.document的批量操作及數據路由原理
- 6.倒排索引
- 十七.分布式相關
- 1.分布式事務解決方案一網打盡
- 2.關于xxx怎么保證高可用的問題
- 3.一致性hash原理與實現
- 4.微服務注冊中心 Nacos 比 Eureka的優勢
- 5.Raft 協議算法
- 6.為什么微服務架構中需要網關
- 0.CAP與BASE理論
- 十八.Dubbo
- 1.快速掌握Dubbo常規應用
- 2.Dubbo應用進階
- 3.Dubbo調用模塊詳解
- 4.Dubbo調用模塊源碼分析
- 6.Dubbo協議模塊