Java ThreadLocal的使用詳解
ThreadLocal是線程私有的局部變量存儲(chǔ)容器,可以理解成每個(gè)線程都有自己專屬的存儲(chǔ)容器,用來(lái)存儲(chǔ)線程私有變量。ThreadLocal 在日常開(kāi)發(fā)框架中應(yīng)用廣泛,但用不好也會(huì)出現(xiàn)各種問(wèn)題,本文就此講解一下。
1. 應(yīng)用場(chǎng)景
ThreadLocal 的常見(jiàn)應(yīng)用場(chǎng)景有兩種:
- 多線程并發(fā)場(chǎng)景中,用來(lái)保障線程安全。
- 處理較為復(fù)雜的業(yè)務(wù)時(shí),使用ThreadLocal代替參數(shù)的顯示傳遞。
1.1. 保障線程安全
多線程訪問(wèn)同一個(gè)共享變量的時(shí)候容易出現(xiàn)并發(fā)問(wèn)題,特別是多個(gè)線程對(duì)一個(gè)變量進(jìn)行寫入的時(shí)候,為了保證線程安全,一般使用者在訪問(wèn)共享變量的時(shí)候需要進(jìn)行額外的同步措施才能保證線程安全性,如:synchronized、Lock之類的鎖。
ThreadLocal是除了加鎖這種同步方式之外的一種,規(guī)避多線程訪問(wèn)出現(xiàn)線程不安全的方法。當(dāng)我們?cè)趧?chuàng)建一個(gè)變量后,如果每個(gè)線程對(duì)其進(jìn)行訪問(wèn)的時(shí)候訪問(wèn)的都是線程自己的變量,這樣就不會(huì)存在線程不安全問(wèn)題。
ThreadLocal是JDK包提供的,它提供線程本地變量,如果創(chuàng)建一個(gè)ThreadLocal變量,那么訪問(wèn)這個(gè)變量的每個(gè)線程都會(huì)有這個(gè)變量的一個(gè)副本,在實(shí)際多線程操作的時(shí)候,操作的是自己本地內(nèi)存中的變量,從而規(guī)避了線程安全問(wèn)題。
1.2. 顯示傳遞參數(shù)
這里舉幾個(gè)例子:
示例1:獲取接口的當(dāng)前請(qǐng)求用戶
在后臺(tái)接口業(yè)務(wù)邏輯的全過(guò)程中,如果需要在多個(gè)地方獲取當(dāng)前請(qǐng)求用戶的信息。通常的一種做法就是:在接口請(qǐng)求時(shí),通過(guò)過(guò)濾器、攔截器、AOP等方式,從session或token中獲取當(dāng)前用戶信息,存入ThreadLocal中。
在整個(gè)接口處理過(guò)程中,如果沒(méi)有另外創(chuàng)建線程,都可以直接從ThreadLocal變量中獲取當(dāng)前用戶,而無(wú)需再?gòu)腟ession、token中驗(yàn)證和獲取用戶。這種方案設(shè)計(jì)不僅提高性能,最重要的是將原本復(fù)雜的邏輯和代碼實(shí)現(xiàn),變得簡(jiǎn)潔明了。例如下面的這個(gè)例子:
(1)定義ThreadLocal變量:UserProfileThread.java
public class UserProfileThread { private static ThreadLocal<UserProfile> USER_PROFILE_TL =new ThreadLocal<>(); public static void setUserProfile(UserProfile userProfile){ USER_PROFILE_TL.set(userProfile); } public static UserProfile getUserProfile() { return USER_PROFILE_TL.get(); } public static String getCurrentUser() { return Optional.ofNullable(USER_PROFILE_TL.get()) .map(UserProfile::getUid) .orElse(UserProfile.ANONYMOUS_USER); } }
(2)在過(guò)濾器中設(shè)置變量值:
@Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { UserProfile userProfile = null; // ... 驗(yàn)證和獲取用戶信息 userProfile UserProfileThread.setUserProfile(userProfile); filterChain.doFilter(servletRequest, servletResponse); }
(3)獲取當(dāng)前用戶信息
//獲取當(dāng)前用戶 String uid=UserProfileThread.getCurrentUser(); //獲取當(dāng)前用戶對(duì)象 UserProfile user=UserProfileThread.getUserProfile();
示例2:spring框架中保證數(shù)據(jù)庫(kù)事務(wù)在同一個(gè)連接下執(zhí)行
要想實(shí)現(xiàn)jdbc事務(wù), 就必須是在同一個(gè)連接對(duì)象中操作,多個(gè)連接下事務(wù)就會(huì)不可控,需要借助分布式事務(wù)完成。那spring框架如何保證數(shù)據(jù)庫(kù)事務(wù)在同一個(gè)連接下執(zhí)行的呢?
DataSourceTransactionManager 是spring的數(shù)據(jù)源事務(wù)管理器,它會(huì)在你調(diào)用getConnection()的時(shí)候從數(shù)據(jù)庫(kù)連接池中獲取一個(gè)connection, 然后將其與ThreadLocal綁定,事務(wù)完成后解除綁定。這樣就保證了事務(wù)在同一連接下完成。
2. 實(shí)現(xiàn)原理
ThreadLocal類提供set/get方法存儲(chǔ)和獲取value值,但實(shí)際上ThreadLocal類并不存儲(chǔ)value值,真正存儲(chǔ)是靠ThreadLocalMap這個(gè)類。
每個(gè)線程實(shí)例都對(duì)應(yīng)一個(gè)TheadLocalMap實(shí)例,我們可以在同一個(gè)線程里實(shí)例化很多個(gè)ThreadLocal來(lái)存儲(chǔ)很多種類型的值,這些ThreadLocal實(shí)例分別作為key,對(duì)應(yīng)各自的value,最終存儲(chǔ)在Entry table數(shù)組中。
我們看看ThreadLocal的set方法:
public class ThreadLocal<T> { public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); } // 省略其他方法 }
set的邏輯比較簡(jiǎn)單,就是獲取當(dāng)前線程的ThreadLocalMap,然后往map里添加KV,K是當(dāng)前ThreadLocal實(shí)例,V是我們傳入的value。這里需要注意一下,map的獲取是需要從Thread類對(duì)象里面取,看一下Thread類的定義。
public class Thread implements Runnable { ThreadLocal.ThreadLocalMap threadLocals = null; //省略其他 }
Thread類維護(hù)了一個(gè)ThreadLocalMap的變量引用。
因此,我們可以得出如下結(jié)論:
- 每個(gè)線程是一個(gè)Thread實(shí)例,其內(nèi)部維護(hù)一個(gè)threadLocals的實(shí)例成員,其類型是ThreadLocal.ThreadLocalMap。
- ThreadLocal本身并不是一個(gè)容器,我們存取的value實(shí)際上存儲(chǔ)在ThreadLocalMap中,ThreadLocal只是作為TheadLocalMap的key。
3. 注意事項(xiàng)
ThreadLocal實(shí)例有提供remove()方法,用于回收對(duì)象,清除對(duì)應(yīng)的內(nèi)存占用。這個(gè)方法通常容易被忽略,而導(dǎo)致出現(xiàn)了各種問(wèn)題。如下面幾種:
- 線程復(fù)用:在“獲取接口的當(dāng)前請(qǐng)求用戶”的例子中,Tomcat中是通過(guò)線程池來(lái)處理用戶請(qǐng)求的,而線程池中線程是復(fù)用的??隙〞?huì)出現(xiàn)一個(gè)線程前后被不同用戶的接口請(qǐng)求復(fù)用的情況,因此需要對(duì)用過(guò)的ThreaLocal變量進(jìn)行覆蓋或清除。
- 內(nèi)存溢出:由于ThreadLocalMap的生命周期跟Thread一樣長(zhǎng),如果創(chuàng)建的ThreadLocal變量很多,即對(duì)應(yīng)的key占用的內(nèi)存很大,但卻沒(méi)有手動(dòng)刪除,到了一定程度就會(huì)導(dǎo)致內(nèi)存泄漏。
以上就是Java ThreadLocal的使用詳解的詳細(xì)內(nèi)容,更多關(guān)于Java ThreadLocal的使用的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
ReentrantReadWriteLock不能鎖升級(jí)的原因總結(jié)
今天給大家?guī)?lái)的是關(guān)于Java并發(fā)的相關(guān)知識(shí),文章圍繞著為什么ReentrantReadWriteLock不能鎖升級(jí)展開(kāi),文中有非常詳細(xì)的介紹及代碼示例,需要的朋友可以參考下2021-06-06Java使用POI導(dǎo)出大數(shù)據(jù)量Excel的方法
今天需要寫一個(gè)導(dǎo)出的Excel的功能,但是發(fā)現(xiàn)當(dāng)數(shù)據(jù)量到3萬(wàn)條時(shí),列數(shù)在23列時(shí),內(nèi)存溢出,CPU使用100%,測(cè)試環(huán)境直接炸掉。小編給大家分享基于java使用POI導(dǎo)出大數(shù)據(jù)量Excel的方法,感興趣的朋友一起看看吧2019-11-11詳解SpringCloud Gateway 2020.0.2最新版
這篇文章主要介紹了SpringCloud Gateway 2020.0.2最新版,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-04-04Elasticsearch?mapping?概念及自動(dòng)創(chuàng)建示例
這篇文章主要為大家介紹了Elasticsearch?mapping?概念及自動(dòng)創(chuàng)建示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11springboot2.0配置連接池(hikari、druid)的方法
springboot 2.0 默認(rèn)連接池就是Hikari了,直接在配置文件中輸入配置就可以了,本文通過(guò)實(shí)例代碼給大家介紹了springboot2.0配置連接池(hikari、druid)的方法,感興趣的朋友一起看看吧2021-12-12