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