Java中ThreadLocal的用法及原理詳解
1 ThreadLocal簡介
ThreadLocal中文是:線程局部變量。
- 為什么需要ThreadLocal呢?這是因為在并發(fā)編程中,如果一個類變量被多個線程操作,會造成線程安全問題。例如多個線程使用同一個 SimpleDateFormat 對象。使用ThreadLocal可以讓每個線程擁有線程內(nèi)部的變量,防止多個線程操作一個類變量造成的線程安全問題。
- 那是不是可以讓多線程中的每個任務都創(chuàng)建一個要用的對象呢?這樣做可以避免線程安全問題,但是會造成資源的浪費。例如我們要新建1000個格式化打印時間的任務,每個任務中新建一個 SimpleDateFormat 的對象:
- 我們可以開辟1000個線程分別執(zhí)行上述任務,但這種做法太耗費資源了,不可?。?/li>
- 我們可以使用線程池,例如線程池中有10個線程,然后將這1000個任務放到線程池中執(zhí)行,這樣可以實現(xiàn)打印時間的目的,沒有線程安全問題,但是新建1000個 SimpleDateFormat 對象太浪費了。
- 最好的做法是每個線程中創(chuàng)建一個 SimpleDateFormat 對象,這樣一共只需要創(chuàng)建10個該對象,即保證了線程安全,又節(jié)省了資源。
2 ThreadLocal用法
- 用法一:每個線程需要一個獨享的對象。
- 用法二:每個線程內(nèi)需要保存全局變量。
2.1 用法一:線程獨享對象
請創(chuàng)建1000個格式化打印時間的任務并執(zhí)行。
做法:使用線程池,線程池中開辟10個線程,用這10個線程執(zhí)行這1000個任務,為了防止出現(xiàn)線程安全問題,使用 ThreadLocal 保證每個線程獨享一個 SimpleDateFormat 對象,代碼如下:
/** * 典型場景1:每個線程需要一個獨享的對象 * 利用ThreadLocal,給每個線程分配自己的dateFormat對象,保證了線程安全,高效利用了內(nèi)存 */ public class Main1 { public static ExecutorService tp = Executors.newFixedThreadPool(10); public String date(int seconds) { SimpleDateFormat df = TSF.df.get(); // 獲取當前線程擁有的 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 用法二:線程全局變量
每個線程都會牽涉到三個服務類:Service1、Service2、Service3,這三個類中都會使用到同一個對象。同一個進程內(nèi)部這是一個對象,不同進程之間對象不同,請實現(xiàn)該需求。
- 一種簡單的做法是:我們可以在相應的函數(shù)中進行參數(shù)傳遞但是這樣會導致代碼冗余且不易維護,不可取。
- 做法應該是:使用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原理
- 首先我們應該明確如下類之間的關(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); // 得到當前線程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)獲取當前線程 t ,然后調(diào)用 getMap(t) ,從而得到屬于當前線程 t 的ThreadLocalMap變量 map ;
(2)然后判斷屬于當前線程 t 的 map 是否為空,不空的話從 map 中取出當前鍵值對,這里的鍵是this,也就是說調(diào)用get()方法的變量。對應于用法一的 TSF.df ,對應于用法二的 UserContextHolder.holder 。為空的話則調(diào)用 setInitialValue() ,該函數(shù)會將this作為鍵,重寫的 initialValue() 返回值作為值存入到 map 中。
(3)返回 this 對象對應的值。
無論是用法一,還是用法二,其實本質(zhì)上都在操縱 當前線程 t 的成員變量 threadLocals 。
根據(jù)上述 get() 分析的第(2)點,當我們 new ThreadLocal<>(); 時并沒有向 ThreadLocalMap 中存入鍵值對,只有當調(diào)用 get()、set() 方法時才放入鍵值對,這是懶加載的一種體現(xiàn)。
4 ThreadLocal注意點
ThreadLocalMap
- ThreadLocalMap 和 HashMap 類似,關(guān)于 HashMap 的詳細分析,可以參考:HashMap源碼分析。
- 兩者也有不少區(qū)別:
- 兩者解決哈希沖突的方式不同;
- ThreadLocalMap中的鍵值對,其中鍵為軟引用,值為強引用,但HashMap中鍵值都為強引用。
解決哈希沖突
- ThreadLocalMap采用的是線性探測法,也就是如果發(fā)生沖突,就繼續(xù)找下一個空位置;
- HashMap采用拉鏈法(鏈表+紅黑樹)。
ThreadLocalMap中節(jié)點的鍵值對
如果弱引用對象只與弱引用關(guān)聯(lián),則這個弱引用對象可以被回收。
ThreadLocalMap中的Entry繼承自WeakReference,是弱引用;
每一個Entry都是對key的弱引用;
每個Entry都包含了一個對value的強引用;
value為強引用的原因:因為JVM認為這個引用十分重要,是程序員定義的,不能隨意回收,回收之后可能發(fā)生異響不到的錯誤;
因為值value是強引用,所以可能導致內(nèi)存泄露,最終導致OOM,這是因為:如果線程不終止(比如線程需要保持很久),那么key對應的value就不能被回收,存在以下調(diào)用鏈:Thread---->ThreadLocalMap---->Entry(key為null)---->value。導致value無法回收,日積月累可能造成OOM。
JDK已經(jīng)考慮到了這個問題,所以在Entry的set,remove,rehash方法中會掃描key為null的Entry,并把對應的value設(shè)置為null,這樣value對象就可以被回收。但是這樣做還不足夠,因為我們必須調(diào)用這些方法才能達到上述效果。
為了避免產(chǎn)生內(nèi)存泄露問題,我們在使用完ThreadLocal之后,就應該調(diào)用remove方法(阿里規(guī)約)。例如用法二中 Service3 應該改為:
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(); // 不進行set,直接get main.get(); } }
上述代碼會拋出java.lang.NullPointerException異常,這不是因為get()的原因,而是因為:拆箱時null不能轉(zhuǎn)為基本類型。當返回值改為 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-05Spring mvc Controller和RestFul原理解析
這篇文章主要介紹了Spring mvc Controller和RestFul原理解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-03-03SpringBoot服務端數(shù)據(jù)校驗過程詳解
這篇文章主要介紹了SpringBoot服務端數(shù)據(jù)校驗過程詳解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-02-02SpringBoot實現(xiàn)接口返回數(shù)據(jù)脫敏的代碼示例
在當今的信息化時代,數(shù)據(jù)安全尤為重要,接口返回數(shù)據(jù)脫敏是一種重要的數(shù)據(jù)保護手段,可以防止敏感信息通過接口返回給客戶端,本文旨在探討如何在SpringBoot應用程序中實現(xiàn)接口返回數(shù)據(jù)脫敏,需要的朋友可以參考下2024-07-07