Java ThreadLocal類使用詳解
前言
這幾天看《Java并發(fā)編程之美》的時(shí)候又遇到了ThradLocal這個(gè)類,不得不說(shuō),這個(gè)類在平時(shí)很多場(chǎng)景都遇得到,所以對(duì)其進(jìn)行一個(gè)系統(tǒng)性的學(xué)習(xí),然后再輸出成這篇博客。 那么,什么是ThreadLocal呢? 我們都知道,多線程訪問(wèn)同一個(gè)共享變量很容易出現(xiàn)并發(fā)問(wèn)題,特別是當(dāng)多個(gè)線程對(duì)同一個(gè)共享變量進(jìn)行寫入操作時(shí)。一般為了避免這種情況,我們會(huì)使用synchronized這個(gè)關(guān)鍵字對(duì)代碼塊加鎖。但是這種方式一是會(huì)讓沒(méi)獲取到鎖的線程進(jìn)行阻塞等待,二是需要使用者對(duì)鎖有一定的了解,無(wú)疑提高了編程的難度。其實(shí)ThreadLocal 就可以做這件事情,雖然ThreadLocal 并不是為了解決這個(gè)問(wèn)題而出現(xiàn)的。 ThreadLocal 是JDK 包提供的,它提供了線程本地變量,也就是如果你創(chuàng)建了一個(gè)ThreadLocal 變量,那么訪問(wèn)這個(gè)變量的每個(gè)線程都會(huì)有這個(gè)變量的一個(gè)本地副本。當(dāng)多個(gè)線程操作這個(gè)變量時(shí),實(shí)際操作的是自己本地內(nèi)存里面的變量,從而避免了線程安全問(wèn)題。
如圖所示:

快速開始
接下來(lái)我們就先用一個(gè)簡(jiǎn)單的樣例給大家展示一下ThreadLocal的基本用法
package cuit.pymjl.thradlocal;
/**
* @author Pymjl
* @version 1.0
* @date 2022/7/1 10:56
**/
public class MainTest {
? ?static ThreadLocal<String> threadLocal = new ThreadLocal<>();
?
? ?static void print(String str) {
? ? ? ?//打印當(dāng)前線程中本地內(nèi)存中本地變量的值
? ? ? ?System.out.println(str + " :" + threadLocal.get());
? ? ? ?//清除本地內(nèi)存中的本地變量
? ? ? ?threadLocal.remove();
? }
?
? ?public static void main(String[] args) {
? ? ? ?Thread t1 = new Thread(new Runnable() {
? ? ? ? ? ?@Override
? ? ? ? ? ?public void run() {
? ? ? ? ? ? ? ?//設(shè)置線程1中本地變量的值
? ? ? ? ? ? ? ?threadLocal.set("thread1 local variable");
? ? ? ? ? ? ? ?//調(diào)用打印方法
? ? ? ? ? ? ? ?print("thread1");
? ? ? ? ? ? ? ?//打印本地變量
? ? ? ? ? ? ? ?System.out.println("after remove : " + threadLocal.get());
? ? ? ? ? }
? ? ? });
?
? ? ? ?Thread t2 = new Thread(new Runnable() {
? ? ? ? ? ?@Override
? ? ? ? ? ?public void run() {
? ? ? ? ? ? ? ?//設(shè)置線程1中本地變量的值
? ? ? ? ? ? ? ?threadLocal.set("thread2 local variable");
? ? ? ? ? ? ? ?//調(diào)用打印方法
? ? ? ? ? ? ? ?print("thread2");
? ? ? ? ? ? ? ?//打印本地變量
? ? ? ? ? ? ? ?System.out.println("after remove : " + threadLocal.get());
? ? ? ? ? }
? ? ? });
?
? ? ? ?t1.start();
? ? ? ?t2.start();
? }
}運(yùn)行結(jié)果如圖所示:

ThreadLocal的原理
ThreadLocal相關(guān)類圖
我們先來(lái)看一下ThreadLocal 相關(guān)類的類圖結(jié)構(gòu),如圖所示:

由該圖可知, Thread 類中有一個(gè)threadLocals 和一個(gè)inheritableThreadLocals , 它們都是ThreadLocalMap 類型的變量, 而ThreadLocalMap 是一個(gè)定制化的Hashmap 。在默認(rèn)情況下, 每個(gè)線程中的這兩個(gè)變量都為null ,只有當(dāng)前線程第一次調(diào)用ThreadLocal 的set 或者get 方法時(shí)才會(huì)創(chuàng)建它們。其實(shí)每個(gè)線程的本地變量不是存放在ThreadLocal 實(shí)例里面,而是存放在調(diào)用線程的threadLocals 變量里面。也就是說(shuō), ThreadLocal 類型的本地變量存放在具體的線程內(nèi)存空間中。ThreadLocal 就是一個(gè)工具殼,它通過(guò)set 方法把value 值放入調(diào)用線程的threadLocals 里面并存放起來(lái), 當(dāng)調(diào)用線程調(diào)用它的get 方法時(shí),再?gòu)漠?dāng)前線程的threadLocals 變量里面將其拿出來(lái)使用。 如果調(diào)用線程一直不終止, 那么這個(gè)本地變量會(huì)一直存放在調(diào)用線程的threadLocals 變量里面,所以當(dāng)不需要使用本地變量時(shí)可以通過(guò)調(diào)用ThreadLocal 變量的remove 方法,從當(dāng)前線程的threadLocals 里面刪除該本地變量。另外, Thread 里面的threadLocals 為何被設(shè)計(jì)為map 結(jié)構(gòu)?很明顯是因?yàn)槊總€(gè)線程可以關(guān)聯(lián)多個(gè)ThreadLocal 變量。 接下來(lái)我們來(lái)看看ThreadLocal的set、get、以及remove的源碼
set
? ?public void set(T value) {
? ? ? ?// 1.獲取當(dāng)前線程(調(diào)用者線程)
? ? ? ?Thread t = Thread.currentThread();
? ? ? ?// 2.以當(dāng)前線程作為key值,去查找對(duì)應(yīng)的線程變量,找到對(duì)應(yīng)的map
? ? ? ?ThreadLocalMap map = getMap(t);
? ? ? ?if (map != null) {
? ? ? ? ? ?// 3.如果map不為null,則直接添加元素
? ? ? ? ? ?map.set(this, value);
? ? ? } else {
? ? ? ? ? ?// 4.否則就先創(chuàng)建map,再添加元素
? ? ? ? ? ?createMap(t, value);
? ? ? }
? } ? ?void createMap(Thread t, T firstValue) {
? ? ? ?/**
? ? ? ? * 這里是創(chuàng)建一個(gè)ThreadLocalMap,以當(dāng)前調(diào)用線程的實(shí)例對(duì)象為key,初始值為value
? ? ? ? * 然后放入當(dāng)前線程的Therad.threadLocals屬性里面
? ? ? ? */
? ? ? ?t.threadLocals = new ThreadLocalMap(this, firstValue);
? } ? ?ThreadLocalMap getMap(Thread t) {
? ? ? ?//這里就是直接獲取調(diào)用線程的成員屬性threadlocals
? ? ? ?return t.threadLocals;
? }get
? ?public T get() {
? ? ? ?// 1.獲取當(dāng)前線程
? ? ? ?Thread t = Thread.currentThread();
? ? ? ?// 2.獲取當(dāng)前線程的threadlocals,即ThreadLocalMap
? ? ? ?ThreadLocalMap map = getMap(t);
? ? ? ?// 3.如果map不為null,則直接返回對(duì)應(yīng)的值
? ? ? ?if (map != null) {
? ? ? ? ? ?ThreadLocalMap.Entry e = map.getEntry(this);
? ? ? ? ? ?if (e != null) {
? ? ? ? ? ? ? ?@SuppressWarnings("unchecked")
? ? ? ? ? ? ? ?T result = (T)e.value;
? ? ? ? ? ? ? ?return result;
? ? ? ? ? }
? ? ? }
? ? ? ?// 4.否則,則進(jìn)行初始化
? ? ? ?return setInitialValue();
? }下面是setInitialValue的代碼
private T setInitialValue() {
? ?//初始化屬性,其實(shí)就是null
? ?T value = initialValue();
? ?//獲取當(dāng)前線程
? ?Thread t = Thread.currentThread();
? ?//通過(guò)當(dāng)前線程獲取ThreadLocalMap
? ?ThreadLocalMap map = getMap(t);
? ?//如果map不為null,則直接添加元素
? ?if (map != null) {
? ? ? ?map.set(this, value);
? } else {
? ? ? ?//否則就創(chuàng)建,然后將創(chuàng)建好的map放入當(dāng)前線程的屬性threadlocals
? ? ? ?createMap(t, value);
? }
? ? ? ?//將當(dāng)前ThreadLocal實(shí)例注冊(cè)進(jìn)TerminatingThreadLocal類里面
? ?if (this instanceof TerminatingThreadLocal) {
? ? ? ?TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
? }
? ?return value;
}這里我需要補(bǔ)充說(shuō)明一下TerminatingThreadLocal。這個(gè)類是jdk11新出的,jdk8中并沒(méi)有這個(gè)類,所以在網(wǎng)上很多源碼分析中并未看見這個(gè)類的相關(guān)說(shuō)明。 這個(gè)類我看了一下源碼,其作用應(yīng)該是避免ThreadLocal內(nèi)存泄露的問(wèn)題(感興趣的可以去看看源碼,若有錯(cuò)誤,還請(qǐng)指正)。 這是官方對(duì)其的解釋:
/** * A thread-local variable that is notified when a thread terminates and * it has been initialized in the terminating thread (even if it was * initialized with a null value). * 一個(gè)線程局部變量, * 當(dāng)一個(gè)線程終止并且它已經(jīng)在終止線程中被初始化時(shí)被通知(即使它被初始化為一個(gè)空值)。 */
remove
? ? public void remove() {
? ? ? ? //如果當(dāng)前線程的threadLocals 變量不為空, 則刪除當(dāng)前線程中指定ThreadLocal 實(shí)例的本地變量。
? ? ? ? ThreadLocalMap m = getMap(Thread.currentThread());
? ? ? ? if (m != null) {
? ? ? ? ? ? m.remove(this);
? ? ? ? }
? ? }小結(jié)
在每個(gè)線程內(nèi)部都有一個(gè)名為threadLocals 的成員變量, 該變量的類型為Hash Map , 其中key 為我們定義的ThreadLocal 變量的this 引用, value 則為我們使用set 方法設(shè)置的值。每個(gè)線程的本地變量存放在線程自己的內(nèi)存變量threadLocals 中,如果當(dāng)前線程一直不消亡, 那么這些本地變量會(huì)一直存在, 所以可能會(huì)造成內(nèi)存溢出, 因此使用完畢后要記得調(diào)用ThreadLocal 的remove 方法刪除對(duì)應(yīng)線程的threadLocals 中的本地變量。
ThreadLocal內(nèi)存泄露
為什么會(huì)出現(xiàn)內(nèi)存泄漏?
ThreadLocalMap使用ThreadLocal的弱引用作為key,如果一個(gè)ThreadLocal沒(méi)有外部強(qiáng)引用來(lái)引用它,那么系統(tǒng) GC 的時(shí)候,這個(gè)ThreadLocal勢(shì)必會(huì)被回收,這樣一來(lái),ThreadLocalMap中就會(huì)出現(xiàn)key為null的Entry,就沒(méi)有辦法訪問(wèn)這些key為null的Entry的value,如果當(dāng)前線程再遲遲不結(jié)束的話,這些key為null的Entry的value就會(huì)一直存在一條強(qiáng)引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永遠(yuǎn)無(wú)法回收,造成內(nèi)存泄漏。 其實(shí),ThreadLocalMap的設(shè)計(jì)中已經(jīng)考慮到這種情況,也加上了一些防護(hù)措施:在ThreadLocal的get(),set(),remove()的時(shí)候都會(huì)清除線程ThreadLocalMap里所有key為null的value。 但是這些被動(dòng)的預(yù)防措施并不能保證不會(huì)內(nèi)存泄漏:
- 使用static的ThreadLocal,延長(zhǎng)了ThreadLocal的生命周期,可能導(dǎo)致的內(nèi)存泄漏
- 分配使用了ThreadLocal又不再調(diào)用get(),set(),remove()方法,那么就會(huì)導(dǎo)致內(nèi)存泄漏
為什么使用弱引用?
既然我們都知道,使用了弱引用會(huì)造成ThreadLocalMap內(nèi)存泄漏,那么官方為什么依然使用弱引用而不是強(qiáng)引用呢?這就要從使用弱引用和強(qiáng)引用的區(qū)別來(lái)說(shuō)起了:
- 如果使用強(qiáng)引用:我們知道,ThreadLocalMap的生命周期基本和Thread的生命周期一樣,當(dāng)前線程如果沒(méi)有終止,那么ThreadLocalMap始終不會(huì)被GC回收,而ThreadLocalMap持有對(duì)ThreadLocal的強(qiáng)引用,那么ThreadLocal也不會(huì)被回收,當(dāng)線程生命周期長(zhǎng),如果沒(méi)有手動(dòng)刪除,則會(huì)造成kv累積,從而導(dǎo)致OOM
- 如果使用弱引用:弱引用中的對(duì)象具有很短的聲明周期,因?yàn)樵谙到y(tǒng)GC時(shí),只要發(fā)現(xiàn)弱引用,不管堆空間是否足夠,都會(huì)將對(duì)象進(jìn)行回收。而當(dāng)ThreadLocal的強(qiáng)引用被回收時(shí),ThreadLocalMap所持有的弱引用也會(huì)被回收,如果沒(méi)有手動(dòng)刪除kv,那么會(huì)造成value累積,也會(huì)導(dǎo)致OOM
對(duì)比可知,使用弱引用至少可以保證不會(huì)因?yàn)閙ap的key累積從而導(dǎo)致OOM,而對(duì)應(yīng)的value可以通過(guò)remove,get,set方法在下一次調(diào)用時(shí)被清除??梢?,內(nèi)存泄露的根源不是弱引用,而是ThreadLocalMap的生命周期和Thread一樣長(zhǎng),造成累積導(dǎo)致的
解決方法
既然問(wèn)題的根源是value的累積造成OOM,那么我們對(duì)癥下藥,每次使用完ThreadLocal調(diào)用remove()方法清理掉就行了。
總結(jié)
到此這篇關(guān)于Java ThreadLocal類詳解的文章就介紹到這了,更多相關(guān)Java ThreadLocal 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
關(guān)于Java并發(fā)編程中線程間協(xié)作的兩種方式
這篇文章主要介紹了關(guān)于Java并發(fā)編程中線程間協(xié)作的兩種方式,當(dāng)隊(duì)列滿時(shí),生產(chǎn)者需要等待隊(duì)列有空間才能繼續(xù)往里面放入商品,而在等待的期間內(nèi),生產(chǎn)者必須釋放對(duì)臨界資源的占用權(quán),這是消費(fèi)者模式,需要的朋友可以參考下2023-07-07
Java實(shí)現(xiàn)設(shè)計(jì)模式之責(zé)任鏈模式
責(zé)任鏈模式是一種行為設(shè)計(jì)模式,允許你將請(qǐng)求沿著處理鏈發(fā)送,然后處理者都可對(duì)其進(jìn)行處理,完成后可以再將其傳遞給下一個(gè)處理者。下面將會(huì)舉例說(shuō)明什么是責(zé)任鏈模式,責(zé)任鏈模式該如何使用2022-08-08
Springboot 使用 JSR 303 對(duì) Controller 控制層校驗(yàn)及 Service 服務(wù)層 AOP 校驗(yàn)
這篇文章主要介紹了Springboot 使用 JSR 303 對(duì) Controller 控制層校驗(yàn)及 Service 服務(wù)層 AOP 校驗(yàn) 使用消息資源文件對(duì)消息國(guó)際化的相關(guān)知識(shí),需要的朋友可以參考下2017-12-12
SpringBoot?Validation提示信息國(guó)際化配置方式
這篇文章主要介紹了SpringBoot?Validation提示信息國(guó)際化配置方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-02-02
MyBatis在Mapper中傳遞多個(gè)參數(shù)的四種方法詳解
這篇文章主要介紹了MyBatis在Mapper中傳遞多個(gè)參數(shù)的四種方法,文章通過(guò)代碼示例介紹的非常詳細(xì),具有一定的參考價(jià)值,,需要的朋友可以參考下2023-09-09
SpringBoot JS-SDK自定義微信分享的實(shí)現(xiàn)
這篇文章主要介紹了SpringBoot JS-SDK自定義微信分享的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09

