1. ThreadLocal 是什么
JDK 對ThreadLocal的描述為:
此類提供執行緒區域變數,這些變數與普通變數的不同之處在于,每個訪問一個變數的執行緒(通過其get或set方法)都有自己的、獨立初始化的變數副本,ThreadLocal 實體通常是類中的私有靜態欄位,這些欄位希望將狀態與執行緒(例如,用戶ID或事務ID)相關聯,
說白了,ThreadLocal就是用來存放執行緒自身相關資料的一個容器,這個容器叫做ThreadLocalMap,它是ThreadLocal的一個靜態內部類,同時作為Thread類的一個成員變數,ThreadLocal在使用時,先拿到當前執行緒的成員變數ThreadLocalMap,以當前的ThreadLocal物件作為key,變數作為value 存入ThreadLocalMap, 然后每個執行緒取變數都是從執行緒各自的ThreadLocalMap中取值,自然是執行緒安全的了,因為變數只在自己執行緒的生命周期內起作用,所以說ThreadLocal 提供執行緒區域變數,或者叫執行緒本地變數,
ThreadLocal 的特點有3個:
- 執行緒并發:在多執行緒并發的場景下使用,
- 資料傳遞:通過 ThreadLocal ,在同一個執行緒中,不同組件中傳遞公共變數,
- 執行緒隔離:不同執行緒之間互不干擾,這種變數在執行緒的生命周期內起作用,
2. ThreadLocal 怎么用
ThreadLocal 的常用方法有:
public ThreadLocal():通過構造器創建物件,一般是靜態的,<S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier):初始化一個 ThreadLcoal,void set(T value):設定當前執行緒系結的區域變數,T get():獲取當前執行緒系結的區域變數,void remove():洗掉當前執行緒系結的區域變數,
2.1 使用入門
2.1.1 原始版本
現在模擬一個需求,一個執行緒在業務開始時初始化一個用戶 id(類似在一次web請求中背景關系中初始化一下用戶資訊),業務結束時獲取這個用戶 id(比如用來列印日志,或者作為一個公共變數運用到業務編碼中),存在多個這樣的執行緒,
public class ThreadLocalTest {
private String userId;
private String getUserId() {
return userId;
}
private void setUserId(String userId) {
this.userId = userId;
}
public static void main(String[] args) {
ThreadLocalTest test = new ThreadLocalTest();
for (int i = 1; i < 6; i++) {
Thread thread = new Thread(() -> {
// 當前執行緒初始化userId
test.setUserId(Thread.currentThread().getName() + "的userId");
// 執行其他業務代碼
System.out.println("===執行業務代碼===");
// 當前執行緒獲取userId
System.out.println(Thread.currentThread().getName() + "-->" + test.getUserId());
});
thread.setName("執行緒" + i);
thread.start();
}
}
}
一種可能的結果:
===執行業務代碼===
執行緒2-->執行緒1的userId
===執行業務代碼===
執行緒1-->執行緒3的userId
===執行業務代碼===
執行緒3-->執行緒3的userId
===執行業務代碼===
執行緒4-->執行緒4的userId
由于執行緒調度的不確定性,可能執行緒1運行到一半,切換到了執行緒2,于是執行緒2獲取到的 userId 是執行緒1設定的,也就是說,每個執行緒之間的變數不是隔離的,造成資料錯誤,
2.1.2 ThreadLocal 版本
每個執行緒中的變數都存放到自己的執行緒當中,所以這些變數叫做執行緒區域變數很形象,
public class ThreadLocalTest {
private static ThreadLocal<String> context = new ThreadLocal<>();
private String getUserId() {
return context.get();
}
private void setUserId(String userId) {
context.set(userId);
}
public static void main(String[] args) {
ThreadLocalTest test = new ThreadLocalTest();
for (int i = 1; i < 5; i++) {
Thread thread = new Thread(() -> {
test.setUserId(Thread.currentThread().getName() + "的userId");
System.out.println("===執行業務代碼===");
System.out.println(Thread.currentThread().getName() + "-->" + test.getUserId());
context.remove(); // 使用完清理執行緒區域變數
});
thread.setName("執行緒" + i);
thread.start();
}
}
}
這樣每個執行緒就互不干擾,不會取錯變數值,一種可能的結果如下:
===執行業務代碼===
執行緒1-->執行緒1的userId
===執行業務代碼===
執行緒4-->執行緒4的userId
===執行業務代碼===
執行緒2-->執行緒2的userId
===執行業務代碼===
執行緒3-->執行緒3的userId
2.1.3 synchronized 版本
如果只看結果的正確性,用 synchronized 給業務代碼塊加鎖也是可以完成的,如下:
Thread thread = new Thread(() -> {
synchronized (ThreadLocalTest.class) {
test.setUserId(Thread.currentThread().getName() + "的userId");
System.out.println("===執行業務代碼===");
System.out.println(Thread.currentThread().getName() + "->" + test.getUserId());
}
});
這樣完全可以實作需求,但是 synchronized 的問題是什么呢?我們總說誰誰誰是執行緒安全的類,因為它有 synchronized 修飾,就是因為 synchronized 讓多執行緒變成了單執行緒,它一次只允許一個執行緒執行,它能不安全嗎?但它帶來的代價是性能的下降,它不能并發執行,而 ThreadLocal 可以并發執行,
2.1.4 ThreadLocal 和 synchronized 對比
綜上,synchronized 和 ThreadLocal 兩個處理問題的角度和場景是不同的,
- synchronized 的側重點在于保證操作的原子性,保證并發場景下共享變數的資料一致性,
- ThreadLocal 強調執行緒隔離性,不同的執行緒互不干擾,保證并發場景下資料傳遞的正確性,在web請求背景關系中較為常見,
3. ThreadLocal 原理
3.1 代碼結構
ThreadLocal 的原理要從它的set(T value)、get()方法的原始碼入手,在 set 值的時候,首先會獲取當前執行緒一個的成員變數ThreadLocalMap,ThreadLocalMap的 key 是當前ThreadLocal物件,value 是要存入的值,這個 key 和 value 會存到哪里呢?ThreadLocalMap還有個內部類Entry,這個Entry繼承了WeakReference,key 賦值給弱參考,也就是當前的ThreadLocal物件,value 則賦值給Entry的成員變數value,ThreadLocalMap也是一個哈希表(所謂哈希表,也叫散串列,它基于陣列,通過某種哈希演算法計算出一系列關鍵字對應的散列值,然后以這些散列值作為陣列索引將資料存放到對應位置,達到快速查找的目的),它內部維護一個Entry陣列,來存盤鍵值對,存資料的時候也是通過哈希函式計算ThreadLocal 物件對應的陣列下標,然后放入Entry陣列中,
3.2 記憶體泄漏問題
ThreadLocal 會發生記憶體泄漏嗎?我們結合代碼慢慢分析,
在 2.1.1 節中有這樣的代碼:
public class ThreadLocalTest {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
private void setUserId(String userId) {
threadLocal.set(userId);
}
// ...
}
首先,我們new了一個 ThreadLocal 物件,這里存在一個強參考:threadLocal參考變數指向 ThreadLocal 物件,其次,當其他執行緒執行setUserId方法時,ThreadLocal 的set方法最終是把資料存到了ThreadLocalMap中的Entry,看原始碼我們會發現,存資料最終是呼叫Entry的構造器Entry(ThreadLocal<?> k, Object v)完成的,而k這個引數是傳入的this物件,說明什么?我們使用 ThreadLocal 物件呼叫set,那this肯定是當前new出來的 ThreadLocal 物件!再次說明,我們new出來的 ThreadLocal 物件有兩個參考指向它:
threadLocal變數的強參考,- 在
Entry中 key 的弱參考,
此時再看一張圖(這張圖被廣泛參考,感謝原圖作者??):

- 堆記憶體里面有個 ThreadLocal 物件,它被兩個箭頭指著,實線代表強參考,虛線代表弱參考,
- 有兩個參考鏈,一個是我們手動創建的
threadLocal的參考變數指向的,即圖中的 ThreadLcoal Ref 對應示例代碼中的threadLocal變數;一個是由于呼叫了 ThreadLocal 的set或get方法,初始化了當前執行緒的ThreadLocalMap,再初始化 Map 中的Entry物件,再初始化Entry物件中的 key 和 value,形成一個由當前執行緒物件到它內部變數的參考鏈,即上圖中的 Current Thread Ref,它對應set方法原始碼中的這一行Thread t = Thread.currentThread();中的變數t,
那問題來了,如果這個手動創建的 ThreadLocal 物件 的『參考變數』被回收了,那 ThreadLocal 物件 是不是只剩下Entry中 key 的弱參考了?而弱參考的物件會隨時被 GC 回收,即Entry中的 key 會在 GC 后變為null了,我們知道,ThreadLocalMap的 key 是當前的 ThreadLocal 物件,那 key 為null了之后,就無法獲取到Entry,也取不到 value 的值了,在Entry物件沒有被主動洗掉,或者當前執行緒沒有終結的情況下,該Entry一直處在一個由當前執行緒指向的強參考鏈中,由于這個Entry獲取不到,就一直占用著記憶體,又因為強參考不能被 GC 回收,所以這個Entry就發生了記憶體泄漏,如果這個執行緒是一個普通執行緒,在執行緒終止的時候,整個執行緒物件被回收了,那記憶體泄漏的時間比較短;如果該執行緒一直不終止,比如執行緒池中的核心執行緒,那記憶體泄露問題就一直存在了,
注意,上面說的“如果這個手動創建的 ThreadLocal 物件 的『參考變數』被回收了”,應該會有人疑惑這種情況什么時候會發生呢?第一種情況,手動把這個參考變數置為null,雖然概率小,但也不是沒可能;第二種情況,參考變數是存在堆疊記憶體中,當方法執行完,就會立即回收堆疊記憶體中的參考變數,即堆記憶體中的實際物件失去參考指標了,這種情況就比如 ThreadLocal 是在方法中創建的區域變數,
3.3 為什么使用弱參考
Entry的 key 使用弱參考有記憶體泄漏風險,那為什么 JDK 還是使用弱參考而不是強參考?
我們分兩種情況討論:
- key 使用強參考:ThreadLocal 的參考變數被回收了,這句話意味著什么呢?參考變數被回收了,意味著代碼中不再使用 ThreadLocal 這個物件了,因為要使用 ThreadLocal 這個物件,我們需要用它的參考變數取調
set、get方法,現在參考變數沒了,我們就用不了 ThreadLocal 這個物件了,但問題是,ThreadLocalMap還持有ThreadLocal物件的強參考,當前執行緒到Entry的強參考鏈依然存在,注意,前面提到了,ThreadLocal 物件已經不再使用了,也就是說Entry就獲取不到了,如果Entry沒有手動洗掉,或者執行緒沒有結束,這個沒用的Entry也會一直保留,依然發生記憶體泄漏(要明白記憶體泄漏是物件沒用了,還存在記憶體中不被回收的情況), - key 使用弱參考:前面已經分析過了,ThreadLocal 的參考變數被回收了,
ThreadLocal物件也被回收,導致Entry的 key 變成null,在沒有手動洗掉Entry或執行緒不結束時依然發生記憶體泄漏,
歸根結底,由于ThreadLocalMap的生命周期跟Thread一樣長,在 ThreadLocal 的參考變數消失后,如果執行緒不結束,原來的Entry就不會回收,這就是記憶體泄漏的本質,雖然 ThreadLocal 在每次讀寫資料的時候,都會將key為null的Entry清空,但是,既然 ThreadLocal 的參考變數都消失了,我們也沒機會再set或get了,
那為什么使用弱參考?我也不知道!我還沒想明白,如果正在閱讀的你知道,請你告訴我下,謝謝??,雖然ThreadLocalMap的注釋中解釋了:
To help deal with very large and long-lived usages, the hash table entries use WeakReferences for keys.
為了幫助處理非常大和長期的使用,哈希表條目使用WeakReferences作為鍵,
我覺得沒必要取糾結這個問題,只要規范的使用 ThreadLocal,幾乎不會發生記憶體泄漏,
3.4 如何防止記憶體泄漏
- 把 ThreadLocal 物件申明為類變數,類變數的生命周期跟 JVM 是同步的,這樣 ThreadLocal 的強參考就一直存在,不會被 GC 回收,
Entry的key就不會發生null的情況了, - 使用完 ThreadLocal 后,用
remove()方法,清空當前ThreadLocal 對應的資料,對應的Entry就不占記憶體了,
第一種情況雖熱能避免Entry的key為null的情況,但是如果后續執行緒不再訪問這個 key,且執行緒不結束時,這個 key 對應的資料也會一直存在記憶體中,容易造成記憶體溢位的問題,所以最好的辦法就是在 ThreadLocal 使用完之后,使用remove()方法清除資料,
4. ThreadLocal 如何存多個變數
上面的示例代碼中,ThreadLocal 只存了一個變數,實際情況不可能只存一個吧,多個變數如何存,如何取?
要知道 ThreadLocal 使用set方法存資料時,key 用的this物件,就是當前正在使用的 ThreadLocal 物件,說明一個 ThreadLocal 物件,在一個執行緒中,只能存一個執行緒本地變數,多個執行緒雖然都是用的是一個 key,但是不同的執行緒用的是不同的ThreadLocalMap,
第一種方案是多 new 幾個 ThreadLocal 物件,每個 ThreadLocal 物件對應一個業務變數,
第二種方法就是在給 ThreadLocal 初始化一個HashMap,這是最常規的做法,比如下面:
public class ThreadLocalTest {
private static final ThreadLocal<Map<String, Object>> context =
ThreadLocal.withInitial(HashMap::new);
private String getUserId() {
return String.valueOf(context.get().get("userId"));
}
private void setUserId(String userId) {
context.get().put("userId", userId);
}
public void setUserName(String userName) {
context.get().put("userName", userName);
}
public String getUserName() {
return String.valueOf(context.get().get("userName"));
}
public static void main(String[] args) {
ThreadLocalTest test = new ThreadLocalTest();
for (int i = 1; i < 5; i++) {
Thread thread = new Thread(() -> {
String threadName = Thread.currentThread().getName();
test.setUserId(threadName + "的userId");
test.setUserName(threadName + "的userName");
System.out.println("===執行業務代碼===");
System.out.println(threadName + "-->" + test.getUserId() + "," + test.getUserName());
});
thread.setName("執行緒" + i);
thread.start();
}
}
}
一種可能的結果:
===執行業務代碼===
執行緒2-->執行緒2的userId,執行緒2的userName
===執行業務代碼===
執行緒4-->執行緒4的userId,執行緒4的userName
===執行業務代碼===
執行緒3-->執行緒3的userId,執行緒3的userName
===執行業務代碼===
執行緒1-->執行緒1的userId,執行緒1的userName
5. 為什么用 ThreadLocal
5.1 ThreadLocal的使用場景
執行緒的背景關系傳遞,企業中最常見的是應用到web請求的背景關系,一個 Http 請求會經過一系列攔截器,過濾器最后到達服務層,在這個呼叫鏈路中,會頻繁的使用到一些公共資料,如用戶資訊或請求的ID,把這些公共資料放到 ThreadLocal 中,會在請求的鏈路中非常方便的使用這些資訊,
還有一些框架中會使用 ThreadLocal 來管理資料庫連接,避免了執行緒之間的競爭,比如 Mybatis 就是用 ThreadLocal 來存盤Sqlsession物件,
5.2 使用 ThreadLocal 的好處
使用 ThreadLocal 的好處是并發場景下減少了同一個執行緒內多個函式或組件之間傳遞公共變數的復雜度,且提高了使用這些共享變數的安全性,
本文來自博客園,作者:xfcoding,轉載請注明原文鏈接:https://www.cnblogs.com/cloudrich/p/17431133.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/553405.html
標籤:其他
上一篇:Java的CompletableFuture,Java的多執行緒開發
下一篇:返回列表
