JDK 對 ThreadLocal 類的描述為:
此類提供執行緒區域性變數。這些變數與普通變數的不同之處在於,每個存取一個變數的執行緒(通過其get或set方法)都有自己的、獨立初始化的變數副本。ThreadLocal 範例通常是類中的私有靜態欄位,這些欄位希望將狀態與執行緒(例如,使用者ID或事務ID)相關聯。
說白了,ThreadLocal 就是用來存放執行緒自身相關資料的一個容器,這個容器叫做ThreadLocalMap
,它是 Thread 類的一個成員變數,它本身也是一個雜湊表,key 是 ThreadLocal 本身,value 是存入的變數。也就是說,變數是存在當前執行緒的一個ThreadLocalMap
中,每個執行緒在取這個變數的時候,就是取執行緒自己的本地變數,自然是執行緒安全的了,所以說 ThreadLocal 提供執行緒區域性變數,或者叫本地變數。
ThreadLocal 的特點有3個關鍵點:
ThreadLocal 的常用方法有:
public ThreadLocal()
:通過構造器建立物件。一般是靜態的。<S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier)
:初始化一個 ThreadLcoal。void set(T value)
:設定當前執行緒繫結的區域性變數。T get()
:獲取當前執行緒繫結的區域性變數。void remove()
:刪除當前執行緒當定的區域性變數。現在模擬一個需求,一個執行緒在業務開始時初始化一個使用者 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設定的。也就是說,每個執行緒之間的變數不是隔離的,造成資料錯誤。
每個執行緒中的變數都存放到自己的執行緒當中,所以這些變數叫做執行緒區域性變數很形象。
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
如果只看結果的正確性,用 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 可以並行執行。
綜上,synchronized 和 ThreadLocal 兩個處理問題的角度和場景是不同的。
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
陣列中。
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
中的弱參照。此時再看一張圖(這張圖被廣泛參照,感謝原圖作者