Java中ThreadLocal的用法及原理詳解
1 ThreadLocal簡介
ThreadLocal中文是:線程局部變量。
- 為什么需要ThreadLocal呢?這是因?yàn)樵诓l(fā)編程中,如果一個(gè)類變量被多個(gè)線程操作,會造成線程安全問題。例如多個(gè)線程使用同一個(gè) SimpleDateFormat 對象。使用ThreadLocal可以讓每個(gè)線程擁有線程內(nèi)部的變量,防止多個(gè)線程操作一個(gè)類變量造成的線程安全問題。
- 那是不是可以讓多線程中的每個(gè)任務(wù)都創(chuàng)建一個(gè)要用的對象呢?這樣做可以避免線程安全問題,但是會造成資源的浪費(fèi)。例如我們要新建1000個(gè)格式化打印時(shí)間的任務(wù),每個(gè)任務(wù)中新建一個(gè) SimpleDateFormat 的對象:
- 我們可以開辟1000個(gè)線程分別執(zhí)行上述任務(wù),但這種做法太耗費(fèi)資源了,不可??;
- 我們可以使用線程池,例如線程池中有10個(gè)線程,然后將這1000個(gè)任務(wù)放到線程池中執(zhí)行,這樣可以實(shí)現(xiàn)打印時(shí)間的目的,沒有線程安全問題,但是新建1000個(gè) SimpleDateFormat 對象太浪費(fèi)了。
- 最好的做法是每個(gè)線程中創(chuàng)建一個(gè) SimpleDateFormat 對象,這樣一共只需要?jiǎng)?chuàng)建10個(gè)該對象,即保證了線程安全,又節(jié)省了資源。
2 ThreadLocal用法
- 用法一:每個(gè)線程需要一個(gè)獨(dú)享的對象。
- 用法二:每個(gè)線程內(nèi)需要保存全局變量。
2.1 用法一:線程獨(dú)享對象
請創(chuàng)建1000個(gè)格式化打印時(shí)間的任務(wù)并執(zhí)行。
做法:使用線程池,線程池中開辟10個(gè)線程,用這10個(gè)線程執(zhí)行這1000個(gè)任務(wù),為了防止出現(xiàn)線程安全問題,使用 ThreadLocal 保證每個(gè)線程獨(dú)享一個(gè) SimpleDateFormat 對象,代碼如下:
/** * 典型場景1:每個(gè)線程需要一個(gè)獨(dú)享的對象 * 利用ThreadLocal,給每個(gè)線程分配自己的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)部的,可以定義多個(gè) // 每個(gè)類變量的用法都是類似的,即:TSF.類變量名.get() 根據(jù)類變量名可以知道返回哪個(gè)對象 // 底層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個(gè)不同的時(shí)間。
2.2 用法二:線程全局變量
每個(gè)線程都會牽涉到三個(gè)服務(wù)類:Service1、Service2、Service3,這三個(gè)類中都會使用到同一個(gè)對象。同一個(gè)進(jìn)程內(nèi)部這是一個(gè)對象,不同進(jìn)程之間對象不同,請實(shí)現(xiàn)該需求。
- 一種簡單的做法是:我們可以在相應(yīng)的函數(shù)中進(jìn)行參數(shù)傳遞但是這樣會導(dǎo)致代碼冗余且不易維護(hù),不可取。
- 做法應(yīng)該是:使用ThreadLocal保存屬于每個(gè)線程的對象,然后通過ThreadLocal的 get 方法獲取屬于本線程的對象。
/** * 每個(gè)線程內(nèi)需要保存全局變量 * 同一個(gè)線程內(nèi)該全局信息相同,不同線程間該全局信息不同 * 如下兩個(gè)線程,線程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)部的,可以定義多個(gè) 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是一個(gè)存儲鍵值對Map容器,ThreadLocalMap中還有內(nèi)部類Entry,用于存儲每個(gè)鍵值對,其中鍵為 ThreadLocal 變量,值為用戶傳入的對象。關(guān)系如下:
現(xiàn)在搞清楚了ThreadLocal、ThreadLocalMap之間的關(guān)系,那這兩個(gè)和Thread是什么關(guān)系呢?答案是:Thread中有一個(gè) 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 兩個(gè)函數(shù),這兩個(gè)函數(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<>(); 時(shí)并沒有向 ThreadLocalMap 中存入鍵值對,只有當(dāng)調(diào)用 get()、set() 方法時(shí)才放入鍵值對,這是懶加載的一種體現(xiàn)。
4 ThreadLocal注意點(diǎn)
ThreadLocalMap
- ThreadLocalMap 和 HashMap 類似,關(guān)于 HashMap 的詳細(xì)分析,可以參考:HashMap源碼分析。
- 兩者也有不少區(qū)別:
- 兩者解決哈希沖突的方式不同;
- ThreadLocalMap中的鍵值對,其中鍵為軟引用,值為強(qiáng)引用,但HashMap中鍵值都為強(qiáng)引用。
解決哈希沖突
- ThreadLocalMap采用的是線性探測法,也就是如果發(fā)生沖突,就繼續(xù)找下一個(gè)空位置;
- HashMap采用拉鏈法(鏈表+紅黑樹)。
ThreadLocalMap中節(jié)點(diǎn)的鍵值對
如果弱引用對象只與弱引用關(guān)聯(lián),則這個(gè)弱引用對象可以被回收。
ThreadLocalMap中的Entry繼承自WeakReference,是弱引用;
每一個(gè)Entry都是對key的弱引用;
每個(gè)Entry都包含了一個(gè)對value的強(qiáng)引用;
value為強(qiáng)引用的原因:因?yàn)镴VM認(rèn)為這個(gè)引用十分重要,是程序員定義的,不能隨意回收,回收之后可能發(fā)生異響不到的錯(cuò)誤;
因?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)考慮到了這個(gè)問題,所以在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)椋翰鹣鋾r(shí)null不能轉(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二級緩存,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-05-05Spring?Boot項(xiàng)目傳參校驗(yàn)的最佳實(shí)踐指南
有參數(shù)傳遞的地方都少不了參數(shù)校驗(yàn),在web開發(fā)中前端的參數(shù)校驗(yàn)是為了用戶體驗(yàn),后端的參數(shù)校驗(yàn)是為了安全,下面這篇文章主要給大家介紹了關(guān)于Spring?Boot項(xiàng)目傳參校驗(yàn)的最佳實(shí)踐,需要的朋友可以參考下2022-04-04Spring mvc Controller和RestFul原理解析
這篇文章主要介紹了Spring mvc Controller和RestFul原理解析,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-03-03SpringBoot服務(wù)端數(shù)據(jù)校驗(yàn)過程詳解
這篇文章主要介紹了SpringBoot服務(wù)端數(shù)據(jù)校驗(yàn)過程詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-02-02SpringBoot實(shí)現(xiàn)接口返回?cái)?shù)據(jù)脫敏的代碼示例
在當(dāng)今的信息化時(shí)代,數(shù)據(jù)安全尤為重要,接口返回?cái)?shù)據(jù)脫敏是一種重要的數(shù)據(jù)保護(hù)手段,可以防止敏感信息通過接口返回給客戶端,本文旨在探討如何在SpringBoot應(yīng)用程序中實(shí)現(xiàn)接口返回?cái)?shù)據(jù)脫敏,需要的朋友可以參考下2024-07-07java將指定目錄下文件復(fù)制到目標(biāo)文件夾的幾種小方法
在Java中有多種方法可以實(shí)現(xiàn)文件的復(fù)制,這篇文章主要給大家介紹了關(guān)于java將指定目錄下文件復(fù)制到目標(biāo)文件夾的幾種小方法,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-01-01