并發編程之ThreadLocal
前言
當多執行緒訪問共享可變資料時,涉及到執行緒間同步的問題,并不是所有時候,都要用到共享資料,所以就需要執行緒封閉出場了,
資料都被封閉在各自的執行緒之中,就不需要同步,這種通過將資料封閉在執行緒中而避免使用同步的技術稱為執行緒封閉,
本文主要介紹執行緒封閉中的其中一種體現:ThreadLocal,將會介紹什么是 ThreadLocal;從 ThreadLocal 原始碼角度分析,最后介紹 ThreadLocal 的應用場景,
什么是ThreadLocal
ThreadLocal 是 Java 里一種特殊變數,它是一個執行緒級別變數,每個執行緒都有一個 ThreadLocal 就是每個執行緒都擁有了自己獨立的一個變數,競態條件被徹底消除了,在并發模式下是絕對安全的變數,
可以通過 ThreadLocal
會自動在每一個執行緒上創建一個 T 的副本,副本之間彼此獨立,互不影響,可以用 ThreadLocal 存盤一些引數,以便在執行緒中多個方法中使用,用以代替方法傳參的做法,
下面通過例子來了解下 ThreadLocal:
@Slf4j
public class ThreadLocalUtil {
/**
* static 確保全域只有一個保存 String 物件的 ThreadLocal 實體
* final 確保 ThreadLocal 實體不可更改 防止被意外改變 導致存入的值和取出的值不一致,并且還能防止 ThreadLocal 實體記憶體泄漏
*/
private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
// 主執行緒設定值
THREAD_LOCAL.set("主執行緒值");
String v = THREAD_LOCAL.get();
log.info("執行緒pool-1-thread-1執行之前," + Thread.currentThread().getName() + " 執行緒獲取到的值為:{}", v);
ExecutorService executorService = Executors.newFixedThreadPool(1);
executorService.submit(() -> {
String s = THREAD_LOCAL.get();
log.info(Thread.currentThread().getName() + " 執行緒獲取到的值為:{}", s);
// 子執行緒設定值
THREAD_LOCAL.set("子執行緒值");
s = THREAD_LOCAL.get();
log.info("重新設定值之后," + Thread.currentThread().getName() + " 執行緒獲取到的值為:{}", s);
log.info(Thread.currentThread().getName() + "線 程執行結束");
THREAD_LOCAL.remove();
});
// 等待子執行緒執行結束
Thread.sleep(1000L);
v = THREAD_LOCAL.get();
log.info("執行緒pool-1-thread-1執行之后," + Thread.currentThread().getName() + " 執行緒獲取到的值為:{}", v);
THREAD_LOCAL.remove();
}
}
首先通過 static final 定義了一個 THREAD_LOCAL 變數,其中 static 是為了確保全域只有一個保存 String 物件的 ThreadLocal 實體;
final 確保 ThreadLocal 的實體不可更改,防止被意外改變,導致放入的值和取出來的不一致,另外還能防止 ThreadLocal 的記憶體泄漏,上面的例子是演示在不同的執行緒中獲取它會得到不同的結果,運行結果如下:
14:33:42.176 [main] INFO com.linkcld.redis.util.ThreadLocalUtil - 執行緒pool-1-thread-1執行之前,main 執行緒獲取到的值為:主執行緒值
14:33:42.307 [pool-1-thread-1] INFO com.linkcld.redis.util.ThreadLocalUtil - pool-1-thread-1 執行緒獲取到的值為:null
14:33:42.307 [pool-1-thread-1] INFO com.linkcld.redis.util.ThreadLocalUtil - 重新設定值之后,pool-1-thread-1 執行緒獲取到的值為:子執行緒值
14:33:42.307 [pool-1-thread-1] INFO com.linkcld.redis.util.ThreadLocalUtil - pool-1-thread-1線 程執行結束
14:33:43.307 [main] INFO com.linkcld.redis.util.ThreadLocalUtil - 執行緒pool-1-thread-1執行之后,main 執行緒獲取到的值為:主執行緒值
首先在 pool-1-thread-1 執行緒執行之前,先給 THREAD_LOCAL 設定為 主執行緒值,然后可以取到這個值,然后通過創建一個新的執行緒以后去取這個值,發現新執行緒取到的為 null,意外著這個變數在不同執行緒中取到的值是不同的,不同執行緒之間對于 ThreadLocal 會有對應的副本,接著在執行緒 pool-1-thread-1 中執行對 THREAD_LOCAL 的修改,將值改為 子執行緒值,可以發現執行緒 pool-1-thread-1 獲取的值變為了 子執行緒值,主執行緒依然會讀取到屬于它的副本資料 主執行緒值,這就是執行緒的封閉,
看到這里,我相信大家一定會好奇 ThreadLocal 是如何做到多個執行緒對同一物件 set 操作,但是 get 獲取的值還都是每個執行緒 set 的值呢,接下來就讓我們進入原始碼決議環節:
ThreadLocal 原始碼決議
首先看下 ThreadLocal 都有哪些重要屬性:
// 當前 ThreadLocal 的 hashCode 由 nextHashCode 計算得來的,用于計算當前 ThreadLocal 再 ThreadLocalMap 中的索引位置
private final int threadLocalHashCode = nextHashCode();
// 哈希魔數,主要與斐波那契散列法和黃金分割相關
private static final int HASH_INCREMENT = 0x61c88647;
// 回傳計算出的hash值,其值為 i * HASH_INCREMENT,其中 i 代表呼叫次數
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
// 保證了一臺機器上,每個 ThreadLocal 的 threadLocalHashCode 值是唯一的
rivate static AtomicInteger nextHashCode = new AtomicInteger();
其中的 HASH_INCREMENT 也不是隨便取的,它轉化為十進制是 1640531527,2654435769 轉換成 int 型別就是 -1640531527,2654435769 等于 (√5-1)/2 乘以 2 的 32 次方,(√5-1)/2 就是黃金分割數,近似為 0.618,也就是說 0x61c88647 理解為一個黃金分割數乘以 2 的 32 次方,它可以保證 nextHashCode 生成的哈希值,均勻的分布在 2 的冪次方上,且小于 2 的 32 次方,
下面用例子來證明下:
@Slf4j
public class ThreadLocalUtil2 {
private static final int HASH_INCREMENT = 0x61c88647;
public static void main(String[] args) {
int n = 5;
int max = 2 << (n - 1);
for (int i = 0; i < max; i++) {
System.out.print(i * HASH_INCREMENT & (max - 1));
System.out.print(" ");
}
}
}
運行結果為:
0 7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25
可以發現元素索引值完美的散列在陣列當中,并沒有出現沖突,
ThreadLocalMap
除了上述屬性外,還有一個重要的屬性 ThreadLocalMap,ThreadLocalMap 是 ThreadLocal 的靜態內部類,當一個執行緒有多個 ThreadLocal 時,需要一個容器來管理多個 ThreadLocal,ThreadLocalMap 的作用就是管理執行緒中多個 ThreadLocal,原始碼如下:
static class ThreadLocalMap {
// 鍵值對的存盤結構
static class Entry extends WeakReference<ThreadLocal<?>> {
// ThreadLocal 對應的value值
Object value;
Entry(ThreadLocal<?> k, Object v) {
// ThreadLocal 是弱參考的,當GC時會被回收掉,但是 value 不會被回收
super(k);
value = https://www.cnblogs.com/clearlove5/archive/2021/04/22/v;
}
}
// 默認初始容量 16 必須是2的冪
private static final int INITIAL_CAPACITY = 16;
// 底層時 Entry 陣列,根據需要進行擴容,陣列的大小必須是 2 的冪
private Entry[] table;
// 陣列的大小
private int size = 0;
// 陣列的擴容閾值 默認是 0
private int threshold;
// 陣列擴容閾值為 長度的 2/3
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
}
從原始碼中看到 ThreadLocalMap 其實就是一個簡單的 Map 結構,底層是陣列,有初始化大小,也有擴容閾值大小,陣列的元素是 Entry,Entry 的 key 就是 ThreadLocal 的參考,value 是 ThreadLocal 的值,ThreadLocalMap 解決 hash 沖突的方式采用的是線性探測法,如果發生沖突會繼續尋找下一個空的位置,
這樣的就有可能會發生記憶體泄漏的問題,下面讓我們進行分析:
ThreadLocal 記憶體泄漏
ThreadLocal 在沒有外部強參考時,發生 GC 時會被回收,那么 ThreadLocalMap 中保存的 key 值就變成了 null,而 Entry 又被 threadLocalMap 物件參考,threadLocalMap 物件又被 Thread 物件所參考,那么當 Thread 一直不終結的話,value 物件就會一直存在于記憶體中,也就導致了記憶體泄漏,直至 Thread 被銷毀后,才會被回收,
那么如何避免記憶體泄漏呢?
在使用完 ThreadLocal 變數后,需要我們手動 remove 掉,防止 ThreadLocalMap 中 Entry 一直保持對 value 的強參考,導致 value 不能被回收,其中 remove 原始碼如下所示:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
remove 方法的時序圖如下所示:

remove 方法是先獲取到當前執行緒的 ThreadLocalMap,并且呼叫了它的 remove 方法,從 map 中清理當前 ThreadLocal 物件關聯的鍵值對,這樣 value 就可以被 GC 回收了,
那么 ThreadLocal 是如何實作執行緒隔離的呢?
ThreadLocal 的 set 方法
我們先去看下 ThreadLocal 的 set 方法,原始碼如下:
// set 方法
public void set(T value) {
// 獲取當前thread資訊
Thread t = Thread.currentThread();
// 獲取當前執行緒所在的 ThreadLocalMap
ThreadLocalMap map = getMap(t);
// map 不為空直接set值
if (map != null)
map.set(this, value);
else
// map 為空時,需要先創建 map
createMap(t, value);
}
// 創建map,并保存值
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
set 方法的作用是把我們想要存盤的 value 給保存進去,set 方法的流程主要是:
- 先獲取到當前執行緒的參考
- 利用這個參考來獲取到 ThreadLocalMap
- 如果 map 為空,則去創建一個 ThreadLocalMap
- 如果 map 不為空,就利用 ThreadLocalMap 的 set 方法將 value 添加到 map 中
set 方法的時序圖如下所示:

其中 map 就是我們上面講到的 ThreadLocalMap,可以看到它是通過當前執行緒物件獲取到的 ThreadLocalMap,接下來我們看 getMap方法的源代碼:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
getMap 方法的作用主要是獲取當前執行緒內的 ThreadLocalMap 物件,原來這個 ThreadLocalMap 是執行緒的一個屬性,下面讓我們看看 Thread 中的相關代碼:
// ThreadLocalMap 是執行緒的一個屬性,所以可以保證在多執行緒環境下的執行緒安全
ThreadLocal.ThreadLocalMap threadLocals = null;
可以看出每個執行緒都有 ThreadLocalMap 物件,被命名為 threadLocals,默認為 null,所以每個執行緒的 ThreadLocals 都是隔離獨享的,
呼叫 ThreadLocalMap.set() 時,會把當前 threadLocal 物件作為 key,想要保存的物件作為 value,存入 map,
其中 ThreadLocalMap.set() 的原始碼如下:
// ThreadLocalMap set 方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 計算 key 在陣列中的下標
int i = key.threadLocalHashCode & (len-1);
// 遍歷陣列,找到 threadLocal 物件
for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
// 獲取下標位置處的 ThreadLocal 物件
ThreadLocal<?> k = e.get();
// 鍵值 ThreadLocal 匹配成功的話,直接更新 map 對于下標的 value 值
if (k == key) {
e.value = https://www.cnblogs.com/clearlove5/archive/2021/04/22/value;
return;
}
// 如果 key 不存在的話,說明 ThreadLocal 被GC清理了,直接替換掉
if (k == null) {
// 替換 Entry 方法
replaceStaleEntry(key, value, i);
return;
}
}
// 直接遇到了空槽也沒有匹配到ThreadLocal物件,那么在此空槽處保存ThreadLocal物件和value值
tab[i] = new Entry(key, value);
// 陣列長度+1
int sz = ++size;
// 如果沒有卡槽需要清理并且陣列長度 大于等于 陣列長度的 2/3,陣列需要擴容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
// 擴容的程序就是對所有的 key 進行重新哈希的程序
rehash();
}
// 判斷是否有卡槽需要清理的方法
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;
}
相信到這里,大家應該對 Thread、ThreadLocal 以及 ThreadLocalMap 的關系有了進一步的理解,下圖為三者之間的關系:

ThreadLocal 的 get 方法
了解完 set 方法后,讓我們看下 get 方法,原始碼如下:
// ThreadLocal get 方法
public T get() {
// 獲取當前執行緒
Thread t = Thread.currentThread();
// 獲取ThreadLocalMap 方法
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果 map 為空的話,需要初始化 map
return setInitialValue();
}
// 初始化 map 方法,回傳的值 null
private T setInitialValue() {
T value = https://www.cnblogs.com/clearlove5/archive/2021/04/22/initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
get 方法的主要流程為:
- 先獲取到當前執行緒的參考
- 獲取當前執行緒內部的 ThreadLocalMap
- 如果 map 存在,則獲取當前 ThreadLocal 對應的 value 值
- 如果 map 不存在或者找不到 value 值,則呼叫 setInitialValue() 進行初始化
get 方法的時序圖如下所示:

其中每個 Thread 的 ThreadLocalMap 以 threadLocal 作為 key,保存自己執行緒的 value 副本,也就是保存在每個執行緒中,并沒有保存在 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);
}
ThreadLocalMap 的 resize 方法
當 ThreadLocalMap 中的 ThreadLocal 的個數超過容量閾值時,ThreadLocalMap 就要開始擴容了,我們一起來看下 resize 的源代碼:
// 當需要擴容的時候,需要重新哈希
private void rehash() {
// 清除需要清理的卡槽
expungeStaleEntries();
// 使用較低的閾值進行加倍以避免磁滯,2/3 * size * 3/4 = 1/2 * size
if (size >= threshold - threshold / 4)
// 擴容
resize();
}
// 擴容演算法,陣列容量 * 2
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
// 新的陣列長度 = 舊長度*2
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();
// 如果有需要清理的ThreadLocal,把value置空,方便GC回收
if (k == null) {
e.value = https://www.cnblogs.com/clearlove5/archive/2021/04/22/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;
}
resize 方法主要是進行擴容,同時會將垃圾值標記方便 GC 回收,擴容后陣列大小是原來陣列的兩倍,
ThreadLocal 應用場景
ThreadLocal 的特性也導致了應用場景比較廣泛,主要的應用場景如下:
- 執行緒間資料隔離,各執行緒的 ThreadLocal 互不影響
- 方便同一個執行緒使用某一物件,避免不必要的引數傳遞
- 全鏈路追蹤中的 traceId 或者流程引擎中背景關系的傳遞一般采用 ThreadLocal
- Spring 事務管理器采用了 ThreadLocal
- Spring MVC 的 RequestContextHolder 的實作使用了 ThreadLocal
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/279186.html
標籤:其他
上一篇:python中的常見錯誤
下一篇:pandas(6):數學統計2
