ThreadLocal = 本地執行緒?
一、定義
ThreadLocal
是JDK
包提供的,從名字來看,ThreadLocal
意思就是本地執行緒的意思。
1.1 是什麼?
要想知道他是個啥,我們看看ThreadLocal
的原始碼(基於JDK 1.8
)中對這個類的介紹:
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
大致能夠總結出:
TreadLocal
可以給我們提供一個執行緒內的區域性變數,而且這個變數與一般的變數還不同,它是每個執行緒獨有的,與其他執行緒互不干擾的;ThreadLocal
與普通變數的區別在於:每個使用該變數的執行緒都會初始化一個完全獨立的例項副本。ThreadLocal
變數通常被private static
修飾。當一個執行緒結束時,它所使用的所有ThreadLocal
相對的例項副本都會被回收;- 簡單說
ThreadLocal
就是一種以空間換時間的做法,在每個Thread
裡面維護了一個ThreadLocal.ThreadLocalMap
,把資料進行隔離,每個執行緒的資料不共享,自然就沒有執行緒安全方面的問題了.
1.2 示例
一言不合上程式碼!
//建立ThreadLocal變數 private static ThreadLocal<String> localParam = new ThreadLocal<>(); @Test public void threadLocalDemo() { //建立2個執行緒,分別設定不同的值 new Thread(() -> { localParam.set("Hello 風塵部落格!"); //列印當前執行緒本地記憶體中的localParam變數的值 log.info("{}:{}", Thread.currentThread().getName(), localParam.get()); }, "T1").start(); new Thread(() -> { log.info("{}:{}", Thread.currentThread().getName(), localParam.get()); }, "T2").start(); }
- 結果:
... T1:Hello 風塵部落格!
... T2:null
列印結果證明,T1
執行緒中設定的值無法在T2
取出,證明變數ThreadLocal
在各個執行緒中資料不共享。
1.3 ThreadLocal
的API
ThreadLocal
定義了四個方法:
get()
:返回此執行緒區域性變數當前副本中的值;set(T value)
:將執行緒區域性變數當前副本中的值設定為指定值;initialValue()
:返回此執行緒區域性變數當前副本中的初始值;remove()
:移除此執行緒區域性變數當前副本中的值。
set()
和initialValue()
區別
名稱 | set() |
initialValue() |
---|---|---|
定義 | 為這個執行緒設定一個新值 | 該方法用於設定初始值,並且在呼叫get() 方法時才會被觸發,所以是懶載入。但是如果在get() 之前進行了set() 操作,這樣就不會呼叫 |
區別 | 如果物件生成的時機不由我們控制的時候使用 set() 方式 |
物件初始化的時機由我們控制的時候使用initialValue() 方式 |
二、實現原理
ThreadLocal
有一個特別重要的靜態內部類ThreadLocalMap
,該類才是實現執行緒隔離機制的關鍵。
- 每個執行緒的本地變數不是存放在
ThreadLocal
例項裡面,而是存放在呼叫執行緒的threadLocals
變數裡面,也就是說:ThreadLocal
型別的本地變數存放在具體的執行緒記憶體空間中。
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
Thread
類中有兩個ThreadLocalMap
型別的變數,分別是threadLocals
和inheritableThreadLocals
,而ThreadLocalMap
是一個定製化的Hashmap
,專門用來儲存執行緒本地變數。在預設情況下,每個執行緒中的這兩個變數都為null
,只有當前執行緒第一次呼叫ThreadLocal
的set()
或者get()
方法時才會建立它們。
ThreadLocal
就是一個工具殼,它通過set()
方法把value
值放入呼叫執行緒的threadLocals
裡面並存放起來,當呼叫執行緒呼叫它的get()
方法時,再從當前執行緒的threadLocals
變數裡面將其拿出來使用。如果呼叫執行緒一直不終止,那麼這個本地變數會一直存放在呼叫執行緒的
threadLocals
變數裡面,所以當不需要使用本地變數時可以通過呼叫ThreadLocal
變數的remove()
方法,從當前執行緒的threadLocals
裡面刪除該本地變數。
另外Thread
裡面的threadLocals
被設計為Map
結構是因為每個執行緒可以關聯多個ThreadLocal
變數。
原理小結
- 每個
Thread
維護著一個ThreadLocalMap
的引用; ThreadLocalMap
是ThreadLocal
的內部類,用Entry
來進行儲存;- 呼叫
ThreadLocal
的set()
方法時,實際上就是往ThreadLocalMap
設定值,key
是ThreadLocal
物件,值是傳遞進來的物件; - 呼叫
ThreadLocal
的get()
方法時,實際上就是往ThreadLocalMap
獲取值,key
是ThreadLocal
物件; ThreadLocal
本身並不儲存值,它只是作為一個key
來讓執行緒從ThreadLocalMa
p獲取value
。
三、使用場景
3.1 ThreadLocal
的作用
- 儲存執行緒上下文資訊,在任意需要的地方可以獲取.
由於ThreadLocal
的特性,同一執行緒在某地方進行設定,在隨後的任意地方都可以獲取到。從而可以用來儲存執行緒上下文資訊。
- 執行緒安全的,避免某些情況需要考慮執行緒安全必須同步帶來的效能損失.
3.2 場景一:獨享物件
每個執行緒需要一個獨享物件(通常是工具類,典型需要使用的類有SimpleDateFormat
和Random
)
這類場景阿里規範裡面也提到了:
3.3 場景二:當前資訊需要被執行緒內的所有方法共享
每個執行緒內需要儲存全域性變數(例如在攔截器中獲取使用者資訊),可以讓不同方法直接使用,避免參數傳遞的麻煩。
演示(完整演示見文末Github)
User.java
@Data
public class User {
private String userName;
public User() {
}
public User(String userName) {
this.userName = userName;
}
}
UserContextHolder.java
public class UserContextHolder {
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
Service1.java
public class Service1 {
public void process() {
User user = new User("Van");
//將User物件儲存到 holder 中
UserContextHolder.holder.set(user);
new Service2().process();
}
}
Service2.java
public class Service2 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service2拿到使用者名稱: " + user.getUserName());
new Service3().process();
}
}
Service3.java
public class Service3 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service3拿到使用者名稱: " + user.getUserName());
}
}
- 測試方法
@Test
public void threadForParams() {
new Service1().process();
}
- 結果列印
Service2拿到使用者名稱: Van
Service3拿到使用者名稱: Van
3.4 使用ThreadLocal
的好處
- 達到執行緒安全的目的;
- 不需要加鎖,執行效率高;
- 更加節省記憶體,節省開銷;
- 免去傳參的繁瑣,降低程式碼耦合度。
四、問題
4.1 記憶體洩漏問題
記憶體洩露:某個物件不會再被使用,但是該物件的記憶體卻無法被收回
- 正常情況
當Thread
執行結束後,ThreadLocal
中的value
會被回收,因為沒有任何強引用了。
- 非正常情況
當Thread
一直在執行始終不結束,強引用就不會被回收,存在以下呼叫鏈
Thread-->ThreadLocalMap-->Entry(key為null)-->value
因為呼叫鏈中的 value
和 Thread
存在強引用,所以value
無法被回收,就有可能出現OOM
。
如何避免記憶體洩漏(阿里規範)
呼叫remove()
方法,就會刪除對應的Entry
物件,可以避免記憶體洩漏,所以使用完ThreadLocal
後,要呼叫remove()
方法。
4.2 ThreadLocal
的空指標問題
ThreadLocalNPE.java
public class ThreadLocalNPE {
ThreadLocal<Long> longThreadLocal = new ThreadLocal<>();
public void set() {
longThreadLocal.set(Thread.currentThread().getId());
}
/**
* 當前返回值為基本型別,會報空指標異常,如果改成包裝型別Long就不會出錯
* @return
*/
public long get() {
return longThreadLocal.get();
}
}
- 空指標測試
@Test
public void threadLocalNPE() {
ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
//如果get方法返回值為基本型別,則會報空指標異常,如果是包裝型別就不會出錯
System.out.println(threadLocalNPE.get());
}
如果get()
方法返回值為基本型別,則會報空指標異常;如果是包裝型別就不會出錯。這是因為基本型別和包裝型別存在裝箱和拆箱的關係,所以,我們必須將get()
方法返回值使用包裝型別。
4.3 參考文章
- 再也不學Threadlocal了,看這一篇就忘不掉了(萬字總結)
- 使用 ThreadLocal 一次解決老大難問題
四、技術交流
Github 示例程式碼
- 風塵部落格:https://www.dustyblog.cn
- 風塵部落格-掘金
- 風塵部落格-部落格園
- Github