java編程ThreadLocal上下傳遞源碼解析
引導語
ThreadLocal 提供了一種方式,讓在多線程環(huán)境下,每個線程都可以擁有自己獨特的數(shù)據(jù),并且可以在整個線程執(zhí)行過程中,從上而下的傳遞。
1、用法演示
可能很多同學沒有使用過 ThreadLocal,我們先來演示下 ThreadLocal 的用法,demo 如下:
/**
* ThreadLocal 中保存的數(shù)據(jù)是 Map
*/
static final ThreadLocal<Map<String, String>> context = new ThreadLocal<>();
@Test
public void testThread() {
// 從上下文中拿出 Map
Map<String, String> contextMap = context.get();
if (CollectionUtils.isEmpty(contextMap)) {
contextMap = Maps.newHashMap();
}
contextMap.put("key1", "value1");
context.set(contextMap);
log.info("key1,value1被放到上下文中");
// 從上下文中拿出剛才放進去的數(shù)據(jù)
getFromComtext();
}
private String getFromComtext() {
String value1 = context.get().get("key1");
log.info("從 ThreadLocal 中取出上下文,key1 對應的值為:{}", value1);
return value1;
}
//運行結果:
demo.ninth.ThreadLocalDemo - key1,value1被放到上下文中
demo.ninth.ThreadLocalDemo - 從 ThreadLocal 中取出上下文,key1 對應的值為:value1從運行結果中可以看到,key1 對應的值已經(jīng)從上下文中拿到了。
getFromComtext 方法是沒有接受任何入?yún)⒌?,通過 context.get().get(“key1”) 這行代碼就從上下文中拿到了 key1 的值,接下來我們一起來看下 ThreadLocal 底層是如何實現(xiàn)上下文的傳遞的。
2、類結構
2.1、類泛型
ThreadLocal 定義類時帶有泛型,說明 ThreadLocal 可以儲存任意格式的數(shù)據(jù),源碼如下:
public class ThreadLocal<T> {}
2.2、關鍵屬性
ThreadLocal 有幾個關鍵屬性,我們一一看下:
// threadLocalHashCode 表示當前 ThreadLocal 的 hashCode,用于計算當前 ThreadLocal 在 ThreadLocalMap 中的索引位置
private final int threadLocalHashCode = nextHashCode();
// 計算 ThreadLocal 的 hashCode 值(就是遞增)
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
// static + AtomicInteger 保證了在一臺機器中每個 ThreadLocal 的 threadLocalHashCode 是唯一的
// 被 static 修飾非常關鍵,因為一個線程在處理業(yè)務的過程中,ThreadLocalMap 是會被 set 多個 ThreadLocal 的,多個 ThreadLocal 就依靠 threadLocalHashCode 進行區(qū)分
private static AtomicInteger nextHashCode = new AtomicInteger();還有一個重要屬性:ThreadLocalMap,當一個線程有多個 ThreadLocal 時,需要一個容器來管理多個 ThreadLocal,ThreadLocalMap 的作用就是這個,管理線程中多個 ThreadLocal。
2.2.1、ThreadLocalMap
ThreadLocalMap 本身就是一個簡單的 Map 結構,key 是 ThreadLocal,value 是 ThreadLocal 保存的值,底層是數(shù)組的數(shù)據(jù)結構,源碼如下:
// threadLocalHashCode 表示當前 ThreadLocal 的 hashCode,用于計算當前 ThreadLocal 在 ThreadLocalMap 中的索引位置
private final int threadLocalHashCode = nextHashCode();
// 計算 ThreadLocal 的 hashCode 值(就是遞增)
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
// static + AtomicInteger 保證了在一臺機器中每個 ThreadLocal 的 threadLocalHashCode 是唯一的
// 被 static 修飾非常關鍵,因為一個線程在處理業(yè)務的過程中,ThreadLocalMap 是會被 set 多個 ThreadLocal 的,多個 ThreadLocal 就依靠 threadLocalHashCode 進行區(qū)分
private static AtomicInteger nextHashCode = new AtomicInteger();從源碼中看到 ThreadLocalMap 其實就是一個簡單的 Map 結構,底層是數(shù)組,有初始化大小,也有擴容閾值大小,數(shù)組的元素是 Entry,Entry 的 key 就是 ThreadLocal 的引用,value 是 ThreadLocal 的值。
3、ThreadLocal 是如何做到線程之間數(shù)據(jù)隔離的
ThreadLocal 是線程安全的,我們可以放心使用,主要因為是 ThreadLocalMap 是線程的屬性,我們看下線程 Thread 的源碼,如下:

從上圖中,我們可以看到 ThreadLocals.ThreadLocalMap 和 InheritableThreadLocals.ThreadLocalMap 分別是線程的屬性,所以每個線程的 ThreadLocals 都是隔離獨享的。
父線程在創(chuàng)建子線程的情況下,會拷貝 inheritableThreadLocals 的值,但不會拷貝 threadLocals 的值,源碼如下:

從上圖中我們可以看到,在線程創(chuàng)建時,會把父線程的 inheritableThreadLocals 屬性值進行拷貝。
4、set 方法
set 方法的主要作用是往當前 ThreadLocal 里面 set 值,假如當前 ThreadLocal 的泛型是 Map,那么就是往當前 ThreadLocal 里面 set map,源碼如下:
// set 操作每個線程都是串行的,不會有線程安全的問題
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// 當前 thradLocal 之前有設置值,直接設置,否則初始化
if (map != null)
map.set(this, value);
// 初始化ThreadLocalMap
else
createMap(t, value);
}代碼邏輯比較清晰,我們在一起來看下 ThreadLocalMap.set 的源碼,如下:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 計算 key 在數(shù)組中的下標,其實就是 ThreadLocal 的 hashCode 和數(shù)組大小-1取余
int i = key.threadLocalHashCode & (len-1);
// 整體策略:查看 i 索引位置有沒有值,有值的話,索引位置 + 1,直到找到?jīng)]有值的位置
// 這種解決 hash 沖突的策略,也導致了其在 get 時查找策略有所不同,體現(xiàn)在 getEntryAfterMiss 中
for (Entry e = tab[i];
e != null;
// nextIndex 就是讓在不超過數(shù)組長度的基礎上,把數(shù)組的索引位置 + 1
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
// 找到內(nèi)存地址一樣的 ThreadLocal,直接替換
if (k == key) {
e.value = value;
return;
}
// 當前 key 是 null,說明 ThreadLocal 被清理了,直接替換掉
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 當前 i 位置是無值的,可以被當前 thradLocal 使用
tab[i] = new Entry(key, value);
int sz = ++size;
// 當數(shù)組大小大于等于擴容閾值(數(shù)組大小的三分之二)時,進行擴容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}上面源碼我們注意幾點:
- 是通過遞增的 AtomicInteger 作為 ThreadLocal 的 hashCode 的;
- 計算數(shù)組索引位置的公式是:hashCode 取模數(shù)組大小,由于 hashCode 不斷自增,所以不同的 hashCode 大概率上會計算到同一個數(shù)組的索引位置(但這個不用擔心,在實際項目中,ThreadLocal 都很少,基本上不會沖突);
- 通過 hashCode 計算的索引位置 i 處如果已經(jīng)有值了,會從 i 開始,通過 +1 不斷的往后尋找,直到找到索引位置為空的地方,把當前 ThreadLocal 作為 key 放進去。
好在日常工作中使用 ThreadLocal 時,常常只使用 1~2 個 ThreadLocal,通過 hash 計算出重復的數(shù)組的概率并不是很大。
set 時的解決數(shù)組元素位置沖突的策略,也對 get 方法產(chǎn)生了影響,接著我們一起來看一下 get 方法。
5、get 方法
get 方法主要是從 ThreadLocalMap 中拿到當前 ThreadLocal 儲存的值,源碼如下:
public T get() {
// 因為 threadLocal 屬于線程的屬性,所以需要先把當前線程拿出來
Thread t = Thread.currentThread();
// 從線程中拿到 ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 從 map 中拿到 entry,由于 ThreadLocalMap 在 set 時的 hash 沖突的策略不同,導致拿的時候邏輯也不太一樣
ThreadLocalMap.Entry e = map.getEntry(this);
// 如果不為空,讀取當前 ThreadLocal 中保存的值
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 否則給當前線程的 ThreadLocal 初始化,并返回初始值 null
return setInitialValue();
}接著我們來看下 ThreadLocalMap 的 getEntry 方法,源碼如下:
// 得到當前 thradLocal 對應的值,值的類型是由 thradLocal 的泛型決定的
// 由于 thradLocalMap set 時解決數(shù)組索引位置沖突的邏輯,導致 thradLocalMap get 時的邏輯也是對應的
// 首先嘗試根據(jù) hashcode 取模數(shù)組大小-1 = 索引位置 i 尋找,找不到的話,自旋把 i+1,直到找到索引位置不為空為止
private Entry getEntry(ThreadLocal<?> key) {
// 計算索引位置:ThreadLocal 的 hashCode 取模數(shù)組大小-1
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// e 不為空,并且 e 的 ThreadLocal 的內(nèi)存地址和 key 相同,直接返回,否則就是沒有找到,繼續(xù)通過 getEntryAfterMiss 方法找
if (e != null && e.get() == key)
return e;
else
// 這個取數(shù)據(jù)的邏輯,是因為 set 時數(shù)組索引位置沖突造成的
return getEntryAfterMiss(key, i, e);
}// 自旋 i+1,直到找到為止
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// 在大量使用不同 key 的 ThreadLocal 時,其實還蠻耗性能的
while (e != null) {
ThreadLocal<?> k = e.get();
// 內(nèi)存地址一樣,表示找到了
if (k == key)
return e;
// 刪除沒用的 key
if (k == null)
expungeStaleEntry(i);
// 繼續(xù)使索引位置 + 1
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}get 邏輯源碼中注釋已經(jīng)寫的很清楚了,我們就不重復說了。
6、擴容
ThreadLocalMap 中的 ThreadLocal 的個數(shù)超過閾值時,ThreadLocalMap 就要開始擴容了,我們一起來看下擴容的邏輯:
//擴容
private void resize() {
// 拿出舊的數(shù)組
Entry[] oldTab = table;
int oldLen = oldTab.length;
// 新數(shù)組的大小為老數(shù)組的兩倍
int newLen = oldLen * 2;
// 初始化新數(shù)組
Entry[] newTab = new Entry[newLen];
int count = 0;
// 老數(shù)組的值拷貝到新數(shù)組上
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 {
// 計算 ThreadLocal 在新數(shù)組中的位置
int h = k.threadLocalHashCode & (newLen - 1);
// 如果索引 h 的位置值不為空,往后+1,直到找到值為空的索引位置
while (newTab[h] != null)
h = nextIndex(h, newLen);
// 給新數(shù)組賦值
newTab[h] = e;
count++;
}
}
}
// 給新數(shù)組初始化下次擴容閾值,為數(shù)組長度的三分之二
setThreshold(newLen);
size = count;
table = newTab;
}源碼注解也比較清晰,我們注意兩點:
- 擴容后數(shù)組大小是原來數(shù)組的兩倍;
- 擴容時是絕對沒有線程安全問題的,因為 ThreadLocalMap 是線程的一個屬性,一個線程同一時刻只能對 ThreadLocalMap 進行操作,因為同一個線程執(zhí)行業(yè)務邏輯必然是串行的,那么操作 ThreadLocalMap 必然也是串行的。
7、總結
ThreadLocal 是非常重要的 API,我們在寫一個中間件的時候經(jīng)常會用到,比如說流程引擎中上下文的傳遞,調(diào)用鏈ID的傳遞等等,非常好用,但坑也很多。
以上就是java編程ThreadLocal上下傳遞源碼解析的詳細內(nèi)容,更多關于java編程ThreadLocal上下傳遞的資料請關注腳本之家其它相關文章!
相關文章
Mybatis-plus操作json字段實戰(zhàn)教程
這篇文章主要介紹了Mybatis-plus操作json字段實戰(zhàn)教程,本文結合實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-02-02
Spring?Data?Jpa?中原生查詢?REGEXP?的使用詳解
這篇文章主要介紹了Spring?Data?Jpa?中原生查詢?REGEXP?的使用詳解,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12
Java中ArrayList去除重復元素(包括字符串和自定義對象)
本文主要介紹了Java中ArrayList去除重復元素(包括字符串和自定義對象)的方法。具有很好的參考價值。下面跟著小編一起來看下吧2017-03-03
Java如何計算兩個時間段內(nèi)的工作日天數(shù)
這篇文章主要介紹了Java如何計算兩個時間段內(nèi)的工作日天數(shù),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-07-07

