Java線程中的ThreadLocal原理及源碼解析
ThreadLocal介紹
ThreadLocal,線程本地變量,ThreadLocal 的作用是為每個(gè)線程保存一份局部變量的引用,實(shí)現(xiàn)多線程之間的數(shù)據(jù)隔離,從而避免了線程不安全情況的發(fā)生。這個(gè)變量保存的值只在線程的生命周期內(nèi)起作用,通過(guò)使用它減少了將執(zhí)行上下文信息傳遞到每個(gè)方法的需要。
如果多個(gè)線程同時(shí)在一個(gè)對(duì)象/實(shí)例上執(zhí)行,它們將共享這個(gè)實(shí)例變量,如果不使用ThreadLocal,就需要在每個(gè)方法上傳遞參數(shù),去跨對(duì)象共享這些變量,同時(shí)還會(huì)導(dǎo)致線程不安全的問(wèn)題。
許多框架使用 ThreadLocals 來(lái)維護(hù)與當(dāng)前線程相關(guān)的一些上下文。例如,當(dāng)前事務(wù)存儲(chǔ)在 ThreadLocal 中時(shí),您不需要通過(guò)每個(gè)方法調(diào)用將其作為參數(shù)傳遞,以防堆棧中的某個(gè)人需要訪問(wèn)它。Web 應(yīng)用程序可能會(huì)將有關(guān)當(dāng)前請(qǐng)求和會(huì)話的信息存儲(chǔ)在 ThreadLocal 中,以便應(yīng)用程序可以輕松訪問(wèn)它們。
ThreadLocal 原理
ThreadLocals 是一種全局變量(盡管由于它們僅限于一個(gè)線程而稍微不那么邪惡),因此在使用它們時(shí)應(yīng)該小心以避免不必要的副作用和內(nèi)存泄漏。
每個(gè)Thread對(duì)象,專門(mén)用一個(gè)ThreadLocalMap來(lái)存儲(chǔ)自己的私有對(duì)象。ThreadLocalMap實(shí)際上就跟我們常用的HashMap類似,存儲(chǔ)在那里的Key-Value形式的數(shù)據(jù)。
ThreadLocal在每次獲取或設(shè)置操作時(shí),都先通過(guò)Thread.currentThread()方法來(lái)獲取當(dāng)前線程,再?gòu)漠?dāng)前線程中獲取ThreadLocalMap。而實(shí)際上,保存的值是通過(guò)ThreadLocalMap來(lái)存儲(chǔ)的。
ThreadLocal對(duì)象可以是多線程共享,但ThreadLocalMap對(duì)象卻是一個(gè)線程獨(dú)享的,每個(gè)線程對(duì)象,創(chuàng)建一個(gè)自己專屬的ThreadLocalMap,與其他Thread對(duì)象創(chuàng)建的ThreadLocalMap不存在一個(gè)單一的關(guān)系。
當(dāng)多個(gè)Thread對(duì)象共同訪問(wèn)同一個(gè)ThreadLocal對(duì)象時(shí),threadLocal只是作為T(mén)hreadLocalMap的Key存在,而不是作為變量的存儲(chǔ)位置。threadLocal的set(方法和get()方法涉及的值是存儲(chǔ)為T(mén)hreadLocalMap的值而ThreadLocalMap是每個(gè)線程專屬的,互不相同的。這就是為什么同ThreadLocal被多線程同時(shí)訪問(wèn),ThreadLocal的值卻互不干擾的原理。
ThreadLocalMap
ThreadLocalMap該類的核心部分是Entry class,它擴(kuò)展了WeakReference. 它確保如果當(dāng)前線程退出,它將被自動(dòng)垃圾收集。這就是為什么它使用ThreadLocalMap而不是簡(jiǎn)單的HashMap. 它將當(dāng)前ThreadLocal及其值作為Entry類的參數(shù)傳遞,所以當(dāng)我們想要獲取值時(shí),我們可以從 中獲取它table.
- 每個(gè)線程中都有一個(gè)自己的 ThreadLocalMap 類對(duì)象,可以將線程自己的對(duì)象保持到其中, 各管各的,線程可以正確的訪問(wèn)到自己的對(duì)象。
- 將一個(gè)共用的 ThreadLocal 靜態(tài)實(shí)例作為 key,將不同對(duì)象的引用保存到不同線程的 ThreadLocalMap中,然后在線程執(zhí)行的各處通過(guò)這個(gè)靜態(tài)ThreadLocal實(shí)例的get()方法取 得自己線程保存的那個(gè)對(duì)象,避免了將這個(gè)對(duì)象作為參數(shù)傳遞的麻煩。
- ThreadLocalMap其實(shí)就是線程里面的一個(gè)屬性,它在Thread類中定義
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal使用場(chǎng)景
代替參數(shù)的顯式傳遞
當(dāng)我們?cè)趯?xiě)API接口的時(shí)候,通常Controller層會(huì)接受來(lái)自前端的入?yún)?,?dāng)這個(gè)接口功能比較復(fù)雜的時(shí)候,可能我們調(diào)用的Service層內(nèi)部還調(diào)用了 很多其他的很多方法,通常情況下,我們會(huì)在每個(gè)調(diào)用的方法上加上需要傳遞的參數(shù)。
但是如果我們將參數(shù)存入ThreadLocal中,那么就不用顯式的傳遞參數(shù)了,而是只需要ThreadLocal中獲取即可。
全局存儲(chǔ)用戶信息
在現(xiàn)在的系統(tǒng)設(shè)計(jì)中,前后端分離已基本成為常態(tài),分離之后如何獲取用戶信息就成了一件麻煩事,通常在用戶登錄后, 用戶信息會(huì)保存在Session或者Token中。這個(gè)時(shí)候,我們?nèi)绻褂贸R?guī)的手段去獲取用戶信息會(huì)很費(fèi)勁,拿Session來(lái)說(shuō),我們要在接口參數(shù)中加上HttpServletRequest對(duì)象,然后調(diào)用 getSession方法,且每一個(gè)需要用戶信息的接口都要加上這個(gè)參數(shù),才能獲取Session,這樣實(shí)現(xiàn)就很麻煩了。 當(dāng)請(qǐng)求到來(lái)時(shí),可以將當(dāng)前Session信息存儲(chǔ)在ThreadLocal中,在請(qǐng)求處理過(guò)程中可以隨時(shí)使用Session信息,每個(gè)請(qǐng)求之間的Session信息互不影響。當(dāng)請(qǐng)求處理完成后通過(guò)remove方法將當(dāng)前Session信息清除即可。
解決線程安全問(wèn)題
在Spring的Web項(xiàng)目中,我們通常會(huì)將業(yè)務(wù)分為Controller層,Service層,Dao層, 我們都知道@Autowired注解默認(rèn)使用單例模式,那么不同請(qǐng)求線程進(jìn)來(lái)之后,由于Dao層使用單例,那么負(fù)責(zé)數(shù)據(jù)庫(kù)連接的Connection也只有一個(gè), 如果每個(gè)請(qǐng)求線程都去連接數(shù)據(jù)庫(kù),那么就會(huì)造成線程不安全的問(wèn)題,Spring是如何解決這個(gè)問(wèn)題的呢?
在Spring項(xiàng)目中Dao層中裝配的Connection肯定是線程安全的,其解決方案就是采用ThreadLocal方法,當(dāng)每個(gè)請(qǐng)求線程使用Connection的時(shí)候, 都會(huì)從ThreadLocal獲取一次,如果為null,說(shuō)明沒(méi)有進(jìn)行過(guò)數(shù)據(jù)庫(kù)連接,連接后存入ThreadLocal中,如此一來(lái),每一個(gè)請(qǐng)求線程都保存有一份 自己的Connection。于是便解決了線程安全問(wèn)題
ThreadLocal源碼
以下是ThreadLocal的get()、set()、remove()方法的代碼
/** * 返回當(dāng)前線程的 this 副本中的值 * 線程局部變量。如果變量沒(méi)有值 * 當(dāng)前線程,首先初始化為返回值 * 通過(guò)調(diào)用 {@link #initialValue} 方法。 * * @return 這個(gè)線程本地的當(dāng)前線程的值 */ public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) return (T)e.value; } return setInitialValue(); } /** * 設(shè)置這個(gè)線程局部變量的當(dāng)前線程的副本 * 到指定值。大多數(shù)子類將不需要 * 重寫(xiě)此方法,僅依賴于 {@link #initialValue} * 設(shè)置線程局部變量值的方法。 * * @param value 要存儲(chǔ)在當(dāng)前線程的副本中的值。 */ public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } /** * 刪除此線程本地的當(dāng)前線程的值 * 多變的。如果此線程局部變量隨后 * {@linkplain #get read} 被當(dāng)前線程讀取,其值為 * 通過(guò)調(diào)用其 {@link #initialValue} 方法重新初始化, * 除非它的值是當(dāng)前線程的 {@linkplain #set set} * 在過(guò)渡期。這可能會(huì)導(dǎo)致多次調(diào)用 * 當(dāng)前線程中的 <tt>initialValue</tt> 方法。 * * @自 1.5 */ public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
ThreadLocal內(nèi)存溢出問(wèn)題
內(nèi)存溢出問(wèn)題模擬
在執(zhí)行main方法前,先使用“-Xmx50m”的參數(shù)來(lái)配置一下 Idea,它表示將程序運(yùn)行的最大內(nèi)存設(shè)置為 50m,如果程序的運(yùn)行超過(guò)這個(gè)值就會(huì)出現(xiàn)內(nèi)存溢出的問(wèn)題
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class ThreadLocalOOMExample { /** * 定義一個(gè) 10m 大的類 */ static class MyTask { // 創(chuàng)建一個(gè) 10m 的數(shù)組(單位轉(zhuǎn)換是 1M -> 1024KB -> 1024*1024B) private byte[] bytes = new byte[10 * 1024 * 1024]; } // 定義 ThreadLocal private static ThreadLocal<MyTask> taskThreadLocal = new ThreadLocal<>(); // 主測(cè)試代碼 public static void main(String[] args) throws InterruptedException { // 創(chuàng)建線程池 ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100)); // 執(zhí)行 10 次調(diào)用 for (int i = 0; i < 10; i++) { // 執(zhí)行任務(wù) executeTask(threadPoolExecutor); Thread.sleep(1000); } } /** * 線程池執(zhí)行任務(wù) * @param threadPoolExecutor 線程池 */ private static void executeTask(ThreadPoolExecutor threadPoolExecutor) { // 執(zhí)行任務(wù) threadPoolExecutor.execute(new Runnable() { @Override public void run() { System.out.println("創(chuàng)建對(duì)象"); // 創(chuàng)建對(duì)象(10M) MyTask myTask = new MyTask(); // 存儲(chǔ) ThreadLocal taskThreadLocal.set(myTask); // 將對(duì)象設(shè)置為 null,表示此對(duì)象不在使用了 myTask = null; } }); } }
原因分析
由于每個(gè)線程 Thread 都擁有一個(gè)數(shù)據(jù)存儲(chǔ)容器 ThreadLocalMap,當(dāng)執(zhí)行 ThreadLocal.set 方法執(zhí)行時(shí),會(huì)將要存儲(chǔ)的值放到 ThreadLocalMap 容器中。而ThreadMap 中有一個(gè) Entry[] 數(shù)組用來(lái)存儲(chǔ)所有的數(shù)據(jù),而 Entry 是一個(gè)包含 key 和 value 的鍵值對(duì),其中 key 為 ThreadLocal 本身,而 value 則是要存儲(chǔ)在 ThreadLocal 中的值。
也就是說(shuō)它們之間的引用關(guān)系是這樣的:Thread -> ThreadLocalMap -> Entry -> Key,Value,因此當(dāng)我們使用線程池來(lái)存儲(chǔ)對(duì)象時(shí),因?yàn)榫€程池有很長(zhǎng)的生命周期,所以線程池會(huì)一直持有 value 值,那么垃圾回收器就無(wú)法回收 value,所以就會(huì)導(dǎo)致內(nèi)存一直被占用,從而導(dǎo)致內(nèi)存溢出問(wèn)題的發(fā)生。
解決方案
嚴(yán)格來(lái)講內(nèi)存溢出并不是 ThreadLocal 的問(wèn)題,而是因?yàn)闆](méi)有正確使用 ThreadLocal 所帶來(lái)的問(wèn)題。想要避免 ThreadLocal 內(nèi)存溢出的問(wèn)題,只需要在使用完 ThreadLocal 后調(diào)用 remove 方法即可。
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class App { /** * 定義一個(gè) 10m 大的類 */ static class MyTask { // 創(chuàng)建一個(gè) 10m 的數(shù)組(單位轉(zhuǎn)換是 1M -> 1024KB -> 1024*1024B) private byte[] bytes = new byte[10 * 1024 * 1024]; } // 定義 ThreadLocal private static ThreadLocal<MyTask> taskThreadLocal = new ThreadLocal<>(); // 測(cè)試代碼 public static void main(String[] args) throws InterruptedException { // 創(chuàng)建線程池 ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100)); // 執(zhí)行 n 次調(diào)用 for (int i = 0; i < 10; i++) { // 執(zhí)行任務(wù) executeTask(threadPoolExecutor); Thread.sleep(1000); } } /** * 線程池執(zhí)行任務(wù) * @param threadPoolExecutor 線程池 */ private static void executeTask(ThreadPoolExecutor threadPoolExecutor) { // 執(zhí)行任務(wù) threadPoolExecutor.execute(new Runnable() { @Override public void run() { System.out.println("創(chuàng)建對(duì)象"); try { // 創(chuàng)建對(duì)象(10M) MyTask myTask = new MyTask(); // 存儲(chǔ) ThreadLocal taskThreadLocal.set(myTask); // 其他業(yè)務(wù)代碼... } finally { // 釋放內(nèi)存 taskThreadLocal.remove(); } } }); } }
到此這篇關(guān)于Java線程中的ThreadLocal原理及源碼解析的文章就介紹到這了,更多相關(guān)ThreadLocal原理及源碼內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java如何使用poi生成簡(jiǎn)單word文檔并導(dǎo)出
這篇文章主要介紹了Java如何使用poi生成簡(jiǎn)單word文檔并導(dǎo)出問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-06-06SpringCloud?Eureka服務(wù)注冊(cè)中心應(yīng)用入門(mén)詳解
這篇文章主要介紹了Spring?Cloud?Eureka服務(wù)注冊(cè)中心入門(mén)流程分析,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-07-07springMVC攔截器HandlerInterceptor用法代碼示例
這篇文章主要介紹了springMVC攔截器HandlerInterceptor用法代碼示例,具有一定借鑒價(jià)值,需要的朋友可以參考下2017-12-12springboot啟動(dòng)mongoDB報(bào)錯(cuò)之禁用mongoDB自動(dòng)配置問(wèn)題
這篇文章主要介紹了springboot啟動(dòng)mongoDB報(bào)錯(cuò)之禁用mongoDB自動(dòng)配置問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-05-05springboot 2.3之后消失的hibernate-validator解決方法
這篇文章主要介紹了springboot 2.3之后消失的hibernate-validator解決方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08Java線程隊(duì)列LinkedBlockingQueue的使用
本文主要介紹了Java線程隊(duì)列LinkedBlockingQueue的使用,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06