前言
(高清無損原圖.pdf關注公眾號后回復 ThreadLocal
獲取,文末有公眾號鏈接)
前幾天寫了一篇AQS
相關的文章:我畫了35張圖就是為了讓你深入 AQS,反響不錯,還上了博客園首頁編輯推薦,有生之年系列呀,哈哈。
這次趁熱打鐵再寫一篇ThreadLocal
的文章,同樣是深入原理,圖文並茂。
全文共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
的點點滴滴。
全文目錄
- ThreadLocal代碼演示
- ThreadLocal的數據結構
- GC 之后key是否為null?
- ThreadLocal.set()方法源碼詳解
- ThreadLocalMap Hash算法
- ThreadLocalMap Hash沖突
- ThreadLocalMap.set()詳解
7.1 ThreadLocalMap.set()原理圖解
7.2 ThreadLocalMap.set()源碼詳解 - ThreadLocalMap過期key的探測式清理流程
- ThreadLocalMap擴容機制
- ThreadLocalMap.get()詳解
10.1 ThreadLocalMap.get()圖解
10.2 ThreadLocalMap.get()源碼詳解 - ThreadLocalMap過期key的啟發式清理流程
- InheritableThreadLocal
- ThreadLocal項目中使用實戰
13.1 ThreadLocal使用場景
13.2 分布式TraceId解決方案
注明: 本文源碼基於JDK 1.8
ThreadLocal代碼演示
我們先看下ThreadLocal
使用示例:
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();
}
}
打印結果:
[一枝花算不算浪漫]
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回收場景)
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();
}
}
}
結果如下:
弱引用key:java.lang.ThreadLocal@433619b6,值:abc
弱引用key:java.lang.ThreadLocal@418a15e3,值:java.lang.ref.SoftReference@bf97a12
--gc后--
弱引用key:null,值:def
如圖所示,因為這里創建的ThreadLocal
並沒有指向任何值,也就是沒有任何引用:
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
方法進行數據處理。
代碼如下:
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
算法來解決散列表數組沖突問題。
int i = key.threadLocalHashCode & (len-1);
ThreadLocalMap
中hash
算法很簡單,這里i
就是當前key在散列表中對應的數組下標位置。
這里最關鍵的就是threadLocalHashCode
值的計算,ThreadLocal
中有一個屬性為HASH_INCREMENT = 0x61c88647
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()
:
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
對應的桶的位置向后查找,找到可以使用的桶。
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
什么情況下桶才是可以使用的呢?
k = key
說明是替換操作,可以使用- 碰到一個過期的桶,執行替換邏輯,占用過期桶
- 查找過程中,碰到桶中
Entry=null
的情況,直接使用
接着就是執行for
循環遍歷,向后查找,我們先看下nextIndex()
、prevIndex()
方法實現:
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
循環中的邏輯:
- 遍歷當前
key
值對應的桶中Entry
數據為空,這說明散列數組這里沒有數據沖突,跳出for
循環,直接set
數據到對應的桶中 - 如果
key
值對應的桶中Entry
數據不為空
2.1 如果k = key
,說明當前set
操作是一個替換操作,做替換邏輯,直接返回
2.2 如果key = null
,說明當前桶位置的Entry
是過期數據,執行replaceStaleEntry()
方法(核心方法),然后返回 for
循環執行完畢,繼續往下執行說明向后迭代的過程中遇到了entry
為null
的情況
3.1 在Entry
為null
的桶中創建一個新的Entry
對象
3.2 執行++size
操作- 調用
cleanSomeSlots()
做一次啟發式清理工作,清理散列數組中Entry
的key
過期的數據
4.1 如果清理工作完成后,未清理到任何數據,且size
超過了閾值(數組長度的2/3),進行rehash()
操作
4.2rehash()
中會先進行一輪探測式清理,清理過期key
,清理完成后如果size >= threshold - threshold / 4,就會執行真正的擴容邏輯(擴容邏輯往后看)
接着重點看下replaceStaleEntry()
方法,replaceStaleEntry()
方法提供替換過期數據的功能,我們可以對應上面第四種情況的原理圖來再回顧下,具體代碼如下:
java.lang.ThreadLocal.ThreadLocalMap.replaceStaleEntry()
:
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
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);
進行啟發式過期數據清理。
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
為當前位置,這個前提是前驅節點掃描時未發現過期數據。
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
往后迭代的過程中如果沒有找到k == key
的數據,且碰到Entry
為null
的數據,則結束當前的迭代操作。此時說明這里是一個添加的邏輯,將新的數據添加到table[staleSlot]
對應的slot
中。
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
最后判斷除了staleSlot
以外,還發現了其他過期的slot
數據,就要開啟清理數據的邏輯:
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
位置,目的是讓正常數據盡可能存放在正確位置或離正確位置更近的位置
在往后迭代的過程中碰到空的槽位,終止探測,這樣一輪探測式清理工作就完成了,接着我們繼續看看具體實現源代碼:
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--
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
}
如果key
沒有過期,重新計算當前key
的下標位置是不是當前槽位下標位置,如果不是,那么說明產生了hash
沖突,此時以新計算出來正確的槽位位置往后迭代,找到最近一個可以存放entry
的位置。
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()
邏輯:
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
接着看下rehash()
具體實現:
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
下次擴容的閾值,具體代碼如下:
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()
:
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.
具體代碼如下:
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
類,我們來看一個例子:
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();
}
}
打印結果:
子線程獲取父類threadLocal數據:null
子線程獲取父類inheritableThreadLocal數據:父類數據:inheritableThreadLocal
實現原理是子線程是通過在父線程中通過調用new Thread()
方法來創建子線程,Thread#init
方法在Thread
的構造方法中被調用。在init
方法中拷貝父線程數據到子線程中:
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遠程調用解決方案
服務發送請求:
@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);
}
}
}
服務接收請求:
@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()
方法:
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
使用即可。
申明
本文章首發自本人公眾號:壹枝花算不算浪漫,如若轉載請標明來源!
感興趣的小伙伴可關注個人公眾號:壹枝花算不算浪漫