Java中ThreadLocal的用法及原理詳解
1 ThreadLocal簡介
ThreadLocal中文是:線程局部變量。
- 為什么需要ThreadLocal呢?這是因?yàn)樵诓l(fā)編程中,如果一個類變量被多個線程操作,會造成線程安全問題。例如多個線程使用同一個 SimpleDateFormat 對象。使用ThreadLocal可以讓每個線程擁有線程內(nèi)部的變量,防止多個線程操作一個類變量造成的線程安全問題。
- 那是不是可以讓多線程中的每個任務(wù)都創(chuàng)建一個要用的對象呢?這樣做可以避免線程安全問題,但是會造成資源的浪費(fèi)。例如我們要新建1000個格式化打印時間的任務(wù),每個任務(wù)中新建一個 SimpleDateFormat 的對象:
- 我們可以開辟1000個線程分別執(zhí)行上述任務(wù),但這種做法太耗費(fèi)資源了,不可??;
- 我們可以使用線程池,例如線程池中有10個線程,然后將這1000個任務(wù)放到線程池中執(zhí)行,這樣可以實(shí)現(xiàn)打印時間的目的,沒有線程安全問題,但是新建1000個 SimpleDateFormat 對象太浪費(fèi)了。
- 最好的做法是每個線程中創(chuàng)建一個 SimpleDateFormat 對象,這樣一共只需要創(chuàng)建10個該對象,即保證了線程安全,又節(jié)省了資源。
2 ThreadLocal用法
- 用法一:每個線程需要一個獨(dú)享的對象。
- 用法二:每個線程內(nèi)需要保存全局變量。
2.1 用法一:線程獨(dú)享對象
請創(chuàng)建1000個格式化打印時間的任務(wù)并執(zhí)行。
做法:使用線程池,線程池中開辟10個線程,用這10個線程執(zhí)行這1000個任務(wù),為了防止出現(xiàn)線程安全問題,使用 ThreadLocal 保證每個線程獨(dú)享一個 SimpleDateFormat 對象,代碼如下:
/**
* 典型場景1:每個線程需要一個獨(dú)享的對象
* 利用ThreadLocal,給每個線程分配自己的dateFormat對象,保證了線程安全,高效利用了內(nèi)存
*/
public class Main1 {
public static ExecutorService tp = Executors.newFixedThreadPool(10);
public String date(int seconds) {
SimpleDateFormat df = TSF.df.get(); // 獲取當(dāng)前線程擁有的 SimpleDateFormat 對象
return df.format(new Date(1000 * seconds));
}
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
int finalI = i;
tp.submit(new Runnable() {
@Override
public void run() {
String date = new Main1().date(finalI);
System.out.println(date);
}
});
}
tp.shutdown();
}
}
class TSF { // ThreadSafeFormatter
// 本類中定義的類變量都是線程內(nèi)部的,可以定義多個
// 每個類變量的用法都是類似的,即:TSF.類變量名.get() 根據(jù)類變量名可以知道返回哪個對象
// 底層map中存在鍵值對:(UTSF.df, 該函數(shù)的返回值)
public static ThreadLocal<SimpleDateFormat> df = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
}
};
}結(jié)果會打印出1000個不同的時間。
2.2 用法二:線程全局變量
每個線程都會牽涉到三個服務(wù)類:Service1、Service2、Service3,這三個類中都會使用到同一個對象。同一個進(jìn)程內(nèi)部這是一個對象,不同進(jìn)程之間對象不同,請實(shí)現(xiàn)該需求。
- 一種簡單的做法是:我們可以在相應(yīng)的函數(shù)中進(jìn)行參數(shù)傳遞但是這樣會導(dǎo)致代碼冗余且不易維護(hù),不可取。
- 做法應(yīng)該是:使用ThreadLocal保存屬于每個線程的對象,然后通過ThreadLocal的 get 方法獲取屬于本線程的對象。
/**
* 每個線程內(nèi)需要保存全局變量
* 同一個線程內(nèi)該全局信息相同,不同線程間該全局信息不同
* 如下兩個線程,線程1保存全局用戶"wxx",線程2保存全局用戶"she"
*/
public class Main2 {
public static void main(String[] args) throws Exception {
new Thread(() -> new Service1().process("wxx")).start();
Thread.sleep(100);
new Thread(() -> new Service1().process("she")).start();
}
}
class Service1 { // Service1 調(diào)用 Service2
public void process(String name) {
User user = new User(name);
UserContextHolder.holder.set(user); // 底層map中存在鍵值對:(UserContextHolder.holder, user)
System.out.println("Service1:" + user.name);
new Service2().process();
}
}
class Service2 { // Service2 調(diào)用 Service3
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service2:" + user.name);
new Service3().process();
}
}
class Service3 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service3:" + user.name);
}
}
class UserContextHolder { // 本類中定義的類變量都是線程內(nèi)部的,可以定義多個
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
String name;
public User(String name) {
this.name = name;
}
}結(jié)果:
Service1:wxx
Service2:wxx
Service3:wxx
Service1:she
Service2:she
Service3:she
3 ThreadLocal原理
- 首先我們應(yīng)該明確如下類之間的關(guān)系:ThreadLocal、ThreadLocalMap、Thread。
- ThreadLocalMap 是 ThreadLocal的內(nèi)部類。ThreadLocalMap是一個存儲鍵值對Map容器,ThreadLocalMap中還有內(nèi)部類Entry,用于存儲每個鍵值對,其中鍵為 ThreadLocal 變量,值為用戶傳入的對象。關(guān)系如下:

現(xiàn)在搞清楚了ThreadLocal、ThreadLocalMap之間的關(guān)系,那這兩個和Thread是什么關(guān)系呢?答案是:Thread中有一個 ThreadLocal.ThreadLocalMap 的變量。如下圖:

public class Thread implements Runnable {
// ...
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
// ....
}接下來我們就可以探究ThreadLocal到底是如何獲取屬于線程內(nèi)部的變量的,關(guān)鍵在于探究ThreadLocal的 get() 方法。該函數(shù)如下:
public class ThreadLocal<T> {
public T get() {
Thread t = Thread.currentThread();
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;
}
}
return setInitialValue();
}
}該函數(shù)中使用到了 getMap 和 setInitialValue 兩個函數(shù),這兩個函數(shù)的定義如下:
public class ThreadLocal<T> {
private T setInitialValue() {
T value = initialValue(); // 用法一 重寫了該方法,由多態(tài)可知,返回重寫的該函數(shù)的返回值
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // 得到當(dāng)前線程t的成員變量 threadLocals
if (map != null)
map.set(this, value); // 向 threadLocals 中放入鍵值對, 關(guān)鍵!!!
else
createMap(t, value);
return value;
}
public void set(T value) { // 用法二調(diào)用了該方法
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value); // 向 threadLocals 中放入鍵值對, 關(guān)鍵!!!
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
}分析 get() 函數(shù)的執(zhí)行流程:
(1)獲取當(dāng)前線程 t ,然后調(diào)用 getMap(t) ,從而得到屬于當(dāng)前線程 t 的ThreadLocalMap變量 map ;
(2)然后判斷屬于當(dāng)前線程 t 的 map 是否為空,不空的話從 map 中取出當(dāng)前鍵值對,這里的鍵是this,也就是說調(diào)用get()方法的變量。對應(yīng)于用法一的 TSF.df ,對應(yīng)于用法二的 UserContextHolder.holder 。為空的話則調(diào)用 setInitialValue() ,該函數(shù)會將this作為鍵,重寫的 initialValue() 返回值作為值存入到 map 中。
(3)返回 this 對象對應(yīng)的值。
無論是用法一,還是用法二,其實(shí)本質(zhì)上都在操縱 當(dāng)前線程 t 的成員變量 threadLocals 。
根據(jù)上述 get() 分析的第(2)點(diǎn),當(dāng)我們 new ThreadLocal<>(); 時并沒有向 ThreadLocalMap 中存入鍵值對,只有當(dāng)調(diào)用 get()、set() 方法時才放入鍵值對,這是懶加載的一種體現(xiàn)。
4 ThreadLocal注意點(diǎn)
ThreadLocalMap
- ThreadLocalMap 和 HashMap 類似,關(guān)于 HashMap 的詳細(xì)分析,可以參考:HashMap源碼分析。
- 兩者也有不少區(qū)別:
- 兩者解決哈希沖突的方式不同;
- ThreadLocalMap中的鍵值對,其中鍵為軟引用,值為強(qiáng)引用,但HashMap中鍵值都為強(qiáng)引用。
解決哈希沖突
- ThreadLocalMap采用的是線性探測法,也就是如果發(fā)生沖突,就繼續(xù)找下一個空位置;
- HashMap采用拉鏈法(鏈表+紅黑樹)。
ThreadLocalMap中節(jié)點(diǎn)的鍵值對
如果弱引用對象只與弱引用關(guān)聯(lián),則這個弱引用對象可以被回收。
ThreadLocalMap中的Entry繼承自WeakReference,是弱引用;
每一個Entry都是對key的弱引用;
每個Entry都包含了一個對value的強(qiáng)引用;
value為強(qiáng)引用的原因:因?yàn)镴VM認(rèn)為這個引用十分重要,是程序員定義的,不能隨意回收,回收之后可能發(fā)生異響不到的錯誤;
因?yàn)橹祐alue是強(qiáng)引用,所以可能導(dǎo)致內(nèi)存泄露,最終導(dǎo)致OOM,這是因?yàn)椋喝绻€程不終止(比如線程需要保持很久),那么key對應(yīng)的value就不能被回收,存在以下調(diào)用鏈:Thread---->ThreadLocalMap---->Entry(key為null)---->value。導(dǎo)致value無法回收,日積月累可能造成OOM。
JDK已經(jīng)考慮到了這個問題,所以在Entry的set,remove,rehash方法中會掃描key為null的Entry,并把對應(yīng)的value設(shè)置為null,這樣value對象就可以被回收。但是這樣做還不足夠,因?yàn)槲覀儽仨氄{(diào)用這些方法才能達(dá)到上述效果。
為了避免產(chǎn)生內(nèi)存泄露問題,我們在使用完ThreadLocal之后,就應(yīng)該調(diào)用remove方法(阿里規(guī)約)。例如用法二中 Service3 應(yīng)該改為:
class Service3 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("Service3:" + user.name);
UserContextHolder.holder.remove(); // 防止內(nèi)存泄露
}
}我們可不可以在新建ThreadLocal并在沒有重寫initialValue()方法后,直接調(diào)用 ThreadLocal 的 get()方法?
可以,只不過會返回 null 。
如下代碼演示了上述描述的問題:
public class ThreadLocalNPE {
ThreadLocal<Long> tl = new ThreadLocal<>();
// public void set() {
// tl.set(Thread.currentThread().getId());
// }
public long get() { // 返回值改為 Long 就沒有NPE異常了
return tl.get(); // tl.get() 為 null
}
public static void main(String[] args) {
ThreadLocalNPE main = new ThreadLocalNPE();
// 不進(jìn)行set,直接get
main.get();
}
}上述代碼會拋出java.lang.NullPointerException異常,這不是因?yàn)間et()的原因,而是因?yàn)椋翰鹣鋾rnull不能轉(zhuǎn)為基本類型。當(dāng)返回值改為 Long 就沒有NPE異常了。
到此這篇關(guān)于Java中ThreadLocal的用法及原理詳解的文章就介紹到這了,更多相關(guān)ThreadLocal的用法及原理內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解Spring boot使用Redis集群替換mybatis二級緩存
本篇文章主要介紹了詳解Spring boot使用Redis集群替換mybatis二級緩存,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-05-05
Spring?Boot項目傳參校驗(yàn)的最佳實(shí)踐指南
有參數(shù)傳遞的地方都少不了參數(shù)校驗(yàn),在web開發(fā)中前端的參數(shù)校驗(yàn)是為了用戶體驗(yàn),后端的參數(shù)校驗(yàn)是為了安全,下面這篇文章主要給大家介紹了關(guān)于Spring?Boot項目傳參校驗(yàn)的最佳實(shí)踐,需要的朋友可以參考下2022-04-04
Spring mvc Controller和RestFul原理解析
這篇文章主要介紹了Spring mvc Controller和RestFul原理解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-03-03
SpringBoot服務(wù)端數(shù)據(jù)校驗(yàn)過程詳解
這篇文章主要介紹了SpringBoot服務(wù)端數(shù)據(jù)校驗(yàn)過程詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-02-02
SpringBoot實(shí)現(xiàn)接口返回數(shù)據(jù)脫敏的代碼示例
在當(dāng)今的信息化時代,數(shù)據(jù)安全尤為重要,接口返回數(shù)據(jù)脫敏是一種重要的數(shù)據(jù)保護(hù)手段,可以防止敏感信息通過接口返回給客戶端,本文旨在探討如何在SpringBoot應(yīng)用程序中實(shí)現(xiàn)接口返回數(shù)據(jù)脫敏,需要的朋友可以參考下2024-07-07
java將指定目錄下文件復(fù)制到目標(biāo)文件夾的幾種小方法
在Java中有多種方法可以實(shí)現(xiàn)文件的復(fù)制,這篇文章主要給大家介紹了關(guān)于java將指定目錄下文件復(fù)制到目標(biāo)文件夾的幾種小方法,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-01-01

