ThreadLocal奪命11連問
點擊關(guān)注公眾號,Java干貨及時送達
前一段時間,有同事使用ThreadLocal踩坑了,正好引起了我的興趣。
所以近期,我抽空把ThreadLocal的源碼再研究了一下,越看越有意思,發(fā)現(xiàn)里面的東西還真不少。
我把精華濃縮了一下,匯集成了下面11個問題,看看你能頂住第幾個?
1. 為什么要用ThreadLocal?
并發(fā)編程是一項非常重要的技術(shù),它讓我們的程序變得更加高效。
但在并發(fā)的場景中,如果有多個線程同時修改公共變量,可能會出現(xiàn)線程安全問題,即該變量最終結(jié)果可能出現(xiàn)異常。
為了解決線程安全問題,JDK出現(xiàn)了很多技術(shù)手段,比如:使用synchronized或Lock,給訪問公共資源的代碼上鎖,保證了代碼的原子性。
但在高并發(fā)的場景中,如果多個線程同時競爭一把鎖,這時會存在大量的鎖等待,可能會浪費很多時間,讓系統(tǒng)的響應(yīng)時間一下子變慢。
因此,JDK還提供了另外一種用空間換時間的新思路:ThreadLocal。
它的核心思想是:共享變量在每個線程都有一個副本,每個線程操作的都是自己的副本,對另外的線程沒有影響。
例如:
@Service
public?class?ThreadLocalService?{
????private?static?final?ThreadLocal?threadLocal?=?new?ThreadLocal<>();
????public?void?add()?{
????????threadLocal.set(1);
????????doSamething();
????????Integer?integer?=?threadLocal.get();
????}
}
2. ThreadLocal的原理是什么?
為了搞清楚ThreadLocal的底層實現(xiàn)原理,我們不得不扒一下源碼。
ThreadLocal的內(nèi)部有一個靜態(tài)的內(nèi)部類叫:ThreadLocalMap。
public?class?ThreadLocal<T>?{
?????...
?????public?T?get()?{
????????//獲取當前線程
????????Thread?t?=?Thread.currentThread();
????????//獲取當前線程的成員變量ThreadLocalMap對象
????????ThreadLocalMap?map?=?getMap(t);
????????if?(map?!=?null)?{
????????????//根據(jù)threadLocal對象從map中獲取Entry對象
????????????ThreadLocalMap.Entry?e?=?map.getEntry(this);
????????????if?(e?!=?null)?{
????????????????@SuppressWarnings("unchecked")
????????????????//獲取保存的數(shù)據(jù)
????????????????T?result?=?(T)e.value;
????????????????return?result;
????????????}
????????}
????????//初始化數(shù)據(jù)
????????return?setInitialValue();
????}
????
????private?T?setInitialValue()?{
????????//獲取要初始化的數(shù)據(jù)
????????T?value?=?initialValue();
????????//獲取當前線程
????????Thread?t?=?Thread.currentThread();
????????//獲取當前線程的成員變量ThreadLocalMap對象
????????ThreadLocalMap?map?=?getMap(t);
????????//如果map不為空
????????if?(map?!=?null)
????????????//將初始值設(shè)置到map中,key是this,即threadLocal對象,value是初始值
????????????map.set(this,?value);
????????else
???????????//如果map為空,則需要創(chuàng)建新的map對象
????????????createMap(t,?value);
????????return?value;
????}
????
????public?void?set(T?value)?{
????????//獲取當前線程
????????Thread?t?=?Thread.currentThread();
????????//獲取當前線程的成員變量ThreadLocalMap對象
????????ThreadLocalMap?map?=?getMap(t);
????????//如果map不為空
????????if?(map?!=?null)
????????????//將值設(shè)置到map中,key是this,即threadLocal對象,value是傳入的value值
????????????map.set(this,?value);
????????else
???????????//如果map為空,則需要創(chuàng)建新的map對象
????????????createMap(t,?value);
????}
????
?????static?class?ThreadLocalMap?{
????????...
?????}
?????...
}
ThreadLocal的get方法、set方法和setInitialValue方法,其實最終操作的都是ThreadLocalMap類中的數(shù)據(jù)。
其中ThreadLocalMap類的內(nèi)部如下:
static?class?ThreadLocalMap?{
????static?class?Entry?extends?WeakReference<ThreadLocal>>?{
????????Object?value;
????????Entry(ThreadLocal>?k,?Object?v)?{
????????????super(k);
????????????value?=?v;
????????}
???}
???...
???private?Entry[]?table;
???...
}
ThreadLocalMap里面包含一個靜態(tài)的內(nèi)部類Entry,該類繼承于WeakReference類,說明Entry是一個弱引用。
ThreadLocalMap內(nèi)部還包含了一個Entry數(shù)組,其中:Entry = ThreadLocal + value。
而ThreadLocalMap被定義成了Thread類的成員變量。
public?class?Thread?implements?Runnable?{
????...
????ThreadLocal.ThreadLocalMap?threadLocals?=?null;
}
下面用一張圖從宏觀上,認識一下ThreadLocal的整體結(jié)構(gòu):
從上圖中看出,在每個Thread類中,都有一個ThreadLocalMap的成員變量,該變量包含了一個Entry數(shù)組,該數(shù)組真正保存了ThreadLocal類set的數(shù)據(jù)。
Entry是由threadLocal和value組成,其中threadLocal對象是弱引用,在GC的時候,會被自動回收。而value就是ThreadLocal類set的數(shù)據(jù)。
下面用一張圖總結(jié)一下引用關(guān)系:
上圖中除了Entry的key對ThreadLocal對象是弱引用,其他的引用都是強引用。
需要特別說明的是,上圖中ThreadLocal對象我畫到了堆上,其實在實際的業(yè)務(wù)場景中不一定在堆上。因為如果ThreadLocal被定義成了static的,ThreadLocal的對象是類共用的,可能出現(xiàn)在方法區(qū)。
3. 為什么用ThreadLocal做key?
不知道你有沒有思考過這樣一個問題:ThreadLocalMap為什么要用ThreadLocal做key,而不是用Thread做key?
如果在你的應(yīng)用中,一個線程中只使用了一個ThreadLocal對象,那么使用Thread做key也未嘗不可。
@Service
public?class?ThreadLocalService?{
????private?static?final?ThreadLocal?threadLocal?=?new?ThreadLocal<>();
}????
但實際情況中,你的應(yīng)用,一個線程中很有可能不只使用了一個ThreadLocal對象。這時使用Thread做key不就出有問題?
@Service
public?class?ThreadLocalService?{
????private?static?final?ThreadLocal?threadLocal1?=?new?ThreadLocal<>();
????private?static?final?ThreadLocal?threadLocal2?=?new?ThreadLocal<>();
????private?static?final?ThreadLocal?threadLocal3?=?new?ThreadLocal<>();
}????
假如使用Thread做key時,你的代碼中定義了3個ThreadLocal對象,那么,通過Thread對象,它怎么知道要獲取哪個ThreadLocal對象呢?
如下圖所示:
因此,不能使用Thread做key,而應(yīng)該改成用ThreadLocal對象做key,這樣才能通過具體ThreadLocal對象的get方法,輕松獲取到你想要的ThreadLocal對象。
如下圖所示:
4. Entry的key為什么設(shè)計成弱引用?
前面說過,Entry的key,傳入的是ThreadLocal對象,使用了WeakReference對象,即被設(shè)計成了弱引用。
那么,為什么要這樣設(shè)計呢?
假如key對ThreadLocal對象的弱引用,改為強引用。
我們都知道ThreadLocal變量對ThreadLocal對象是有強引用存在的。
即使ThreadLocal變量生命周期完了,設(shè)置成null了,但由于key對ThreadLocal還是強引用。
此時,如果執(zhí)行該代碼的線程使用了線程池,一直長期存在,不會被銷毀。
就會存在這樣的強引用鏈:Thread變量 -> Thread對象 -> ThreadLocalMap -> Entry -> key -> ThreadLocal對象。
那么,ThreadLocal對象和ThreadLocalMap都將不會被GC回收,于是產(chǎn)生了內(nèi)存泄露問題。
為了解決這個問題,JDK的開發(fā)者們把Entry的key設(shè)計成了弱引用。
弱引用的對象,在GC做垃圾清理的時候,就會被自動回收了。
如果key是弱引用,當ThreadLocal變量指向null之后,在GC做垃圾清理的時候,key會被自動回收,其值也被設(shè)置成null。
如下圖所示:
接下來,最關(guān)鍵的地方來了。
由于當前的ThreadLocal變量已經(jīng)被指向null了,但如果直接調(diào)用它的get、set或remove方法,很顯然會出現(xiàn)空指針異常。因為它的生命已經(jīng)結(jié)束了,再調(diào)用它的方法也沒啥意義。
此時,如果系統(tǒng)中還定義了另外一個ThreadLocal變量b,調(diào)用了它的get、set或remove,三個方法中的任何一個方法,都會自動觸發(fā)清理機制,將key為null的value值清空。
如果key和value都是null,那么Entry對象會被GC回收。如果所有的Entry對象都被回收了,ThreadLocalMap也會被回收了。
這樣就能最大程度的解決內(nèi)存泄露問題。
需要特別注意的地方是:
key為null的條件是,ThreadLocal變量指向 null,并且key是弱引用。如果ThreadLocal變量沒有斷開對ThreadLocal的強引用,即ThreadLocal變量沒有指向null,GC就貿(mào)然的把弱引用的key回收了,不就會影響正常用戶的使用?如果當前ThreadLocal變量指向 null了,并且key也為null了,但如果沒有其他ThreadLocal變量觸發(fā)get、set或remove方法,也會造成內(nèi)存泄露。
下面看看弱引用的例子:
public?static?void?main(String[]?args)?{
????WeakReference打印結(jié)果:
java.lang.Object@1ef7fe8e
null
傳入WeakReference構(gòu)造方法的是直接new處理的對象,沒有其他引用,在調(diào)用gc方法后,弱引用對象會被自動回收。
但如果出現(xiàn)下面這種情況:
public?static?void?main(String[]?args)?{
????Object?object?=?new?Object();
????WeakReference執(zhí)行結(jié)果:
java.lang.Object@1ef7fe8e
java.lang.Object@1ef7fe8e
先定義了一個強引用object對象,在WeakReference構(gòu)造方法中將object對象的引用作為參數(shù)傳入。這時,調(diào)用gc后,弱引用對象不會被自動回收。
我們的Entry對象中的key不就是第二種情況嗎?在Entry構(gòu)造方法中傳入的是ThreadLocal對象的引用。
如果將object強引用設(shè)置為null:
public?static?void?main(String[]?args)?{
????Object?object?=?new?Object();
????WeakReference執(zhí)行結(jié)果:
java.lang.Object@6f496d9f
java.lang.Object@6f496d9f
null
第二次gc之后,弱引用能夠被正常回收。
由此可見,如果強引用和弱引用同時關(guān)聯(lián)一個對象,那么這個對象是不會被GC回收。也就是說這種情況下Entry的key,一直都不會為null,除非強引用主動斷開關(guān)聯(lián)。
此外,你可能還會問這樣一個問題:Entry的value為什么不設(shè)計成弱引用?
答:Entry的value假如只是被Entry引用,有可能沒被業(yè)務(wù)系統(tǒng)中的其他地方引用。如果將value改成了弱引用,被GC貿(mào)然回收了(數(shù)據(jù)突然沒了),可能會導(dǎo)致業(yè)務(wù)系統(tǒng)出現(xiàn)異常。
而相比之下,Entry的key,管理的地方就非常明確了。
這就是Entry的key被設(shè)計成弱引用,而value沒被設(shè)計成弱引用的原因。
5. ThreadLocal真的會導(dǎo)致內(nèi)存泄露?
通過上面的Entry對象中的key設(shè)置成弱引用,并且使用get、set或remove方法清理key為null的value值,就能徹底解決內(nèi)存泄露問題?
答案是否定的。
如下圖所示:
假如ThreadLocalMap中存在很多key為null的Entry,但后面的程序,一直都沒有調(diào)用過有效的ThreadLocal的get、set或remove方法。
那么,Entry的value值一直都沒被清空。
所以會存在這樣一條強引用鏈:Thread變量 -> Thread對象 -> ThreadLocalMap -> Entry -> value -> Object。
其結(jié)果就是:Entry和ThreadLocalMap將會長期存在下去,會導(dǎo)致內(nèi)存泄露。
6. 如何解決內(nèi)存泄露問題?
前面說過的ThreadLocal還是會導(dǎo)致內(nèi)存泄露的問題,我們有沒有解決辦法呢?
答:有辦法,調(diào)用ThreadLocal對象的remove方法。
不是在一開始就調(diào)用remove方法,而是在使用完ThreadLocal對象之后。列如:
先創(chuàng)建一個CurrentUser類,其中包含了ThreadLocal的邏輯。
public?class?CurrentUser?{
????private?static?final?ThreadLocal?THREA_LOCAL?=?new?ThreadLocal();
????
????public?static?void?set(UserInfo?userInfo)?{
????????THREA_LOCAL.set(userInfo);
????}
????
????public?static?UserInfo?get()?{
???????THREA_LOCAL.get();
????}
????
????public?static?void?remove()?{
???????THREA_LOCAL.remove();
????}
}
然后在業(yè)務(wù)代碼中調(diào)用相關(guān)方法:
public?void?doSamething(UserDto?userDto)?{
???UserInfo?userInfo?=?convert(userDto);
???
???try{
?????CurrentUser.set(userInfo);
?????...
?????
?????//業(yè)務(wù)代碼
?????UserInfo?userInfo?=?CurrentUser.get();
?????...
???}?finally?{
??????CurrentUser.remove();
???}
}
需要我們特別注意的地方是:一定要在finally代碼塊中,調(diào)用remove方法清理沒用的數(shù)據(jù)。如果業(yè)務(wù)代碼出現(xiàn)異常,也能及時清理沒用的數(shù)據(jù)。
remove方法中會把Entry中的key和value都設(shè)置成null,這樣就能被GC及時回收,無需觸發(fā)額外的清理機制,所以它能解決內(nèi)存泄露問題。
7. ThreadLocal是如何定位數(shù)據(jù)的?
前面說過ThreadLocalMap對象底層是用Entry數(shù)組保存數(shù)據(jù)的。
那么問題來了,ThreadLocal是如何定位Entry數(shù)組數(shù)據(jù)的?
在ThreadLocal的get、set、remove方法中都有這樣一行代碼:
int?i?=?key.threadLocalHashCode?&?(len-1);
通過key的hashCode值,與數(shù)組的長度減1。其中key就是ThreadLocal對象,與數(shù)組的長度減1,相當于除以數(shù)組的長度減1,然后取模。
這是一種hash算法。
接下來給大家舉個例子:假設(shè)len=16,key.threadLocalHashCode=31,
于是:int i = 31 & 15 = 15
相當于:int i = 31 % 16 = 15
計算的結(jié)果是一樣的,但是使用與運算效率跟高一些。
為什么與運算效率更高?
答:因為ThreadLocal的初始大小是16,每次都是按2倍擴容,數(shù)組的大小其實一直都是2的n次方。這種數(shù)據(jù)有個規(guī)律就是高位是0,低位都是1。在做與運算時,可以不用考慮高位,因為與運算的結(jié)果必定是0。只需考慮低位的與運算,所以效率更高。
如果使用hash算法定位具體位置的話,就可能會出現(xiàn)hash沖突的情況,即兩個不同的hashCode取模后的值相同。
ThreadLocal是如何解決hash沖突的呢?
我們看看getEntry是怎么做的:
private?Entry?getEntry(ThreadLocal>?key)?{
????//通過hash算法獲取下標值
????int?i?=?key.threadLocalHashCode?&?(table.length?-?1);
????Entry?e?=?table[i];
????//如果下標位置上的key正好是我們所需要尋找的key
????if?(e?!=?null?&&?e.get()?==?key)
????????//說明找到數(shù)據(jù)了,直接返回
????????return?e;
????else
????????//說明出現(xiàn)hash沖突了,繼續(xù)往后找
????????return?getEntryAfterMiss(key,?i,?e);
}
再看看getEntryAfterMiss方法:
private?Entry?getEntryAfterMiss(ThreadLocal>?key,?int?i,?Entry?e)?{
????Entry[]?tab?=?table;
????int?len?=?tab.length;
????//判斷Entry對象如果不為空,則一直循環(huán)
????while?(e?!=?null)?{
????????ThreadLocal>?k?=?e.get();
????????//如果當前Entry的key正好是我們所需要尋找的key
????????if?(k?==?key)
????????????//說明這次真的找到數(shù)據(jù)了
????????????return?e;
????????if?(k?==?null)
????????????//如果key為空,則清理臟數(shù)據(jù)
????????????expungeStaleEntry(i);
????????else
????????????//如果還是沒找到數(shù)據(jù),則繼續(xù)往后找
????????????i?=?nextIndex(i,?len);
????????e?=?tab[i];
????}
????return?null;
}
關(guān)鍵看看nextIndex方法:
private?static?int?nextIndex(int?i,?int?len)?{
????return?((i?+?1?1?:?0);
}
當通過hash算法計算出的下標小于數(shù)組大小,則將下標值加1。否則,即下標大于等于數(shù)組大小,下標變成0了。下標變成0之后,則循環(huán)一次,下標又變成1。。。
尋找的大致過程如下圖所示:
如果找到最后一個,還是沒有找到,則再從頭開始找。
不知道你有沒有發(fā)現(xiàn),它構(gòu)成了一個:環(huán)形。
ThreadLocal從數(shù)組中找數(shù)據(jù)的過程大致是這樣的:
通過key的hashCode取余計算出一個下標。 通過下標,在數(shù)組中定位具體Entry,如果key正好是我們所需要的key,說明找到了,則直接返回數(shù)據(jù)。 如果第2步?jīng)]有找到我們想要的數(shù)據(jù),則從數(shù)組的下標位置,繼續(xù)往后面找。 如果第3步中找key的正好是我們所需要的key,說明找到了,則直接返回數(shù)據(jù)。 如果還是沒有找到數(shù)據(jù),再繼續(xù)往后面找。如果找到最后一個位置,還是沒有找到數(shù)據(jù),則再從頭,即下標為0的位置,繼續(xù)從前往后找數(shù)據(jù)。 直到找到第一個Entry為空為止。
8. ThreadLocal是如何擴容的?
從上面得知,ThreadLocal的初始大小是16。那么問題來了,ThreadLocal是如何擴容的?
在set方法中會調(diào)用rehash方法:
private?void?set(ThreadLocal>?key,?Object?value)?{
????Entry[]?tab?=?table;
????int?len?=?tab.length;
????int?i?=?key.threadLocalHashCode?&?(len-1);
????for?(Entry?e?=?tab[i];
?????????e?!=?null;
?????????e?=?tab[i?=?nextIndex(i,?len)])?{
????????ThreadLocal>?k?=?e.get();
????????if?(k?==?key)?{
????????????e.value?=?value;
????????????return;
????????}
????????if?(k?==?null)?{
????????????replaceStaleEntry(key,?value,?i);
????????????return;
????????}
????}
????tab[i]?=?new?Entry(key,?value);
????int?sz?=?++size;
????if?(!cleanSomeSlots(i,?sz)?&&?sz?>=?threshold)
????????rehash();
}
注意一下,其中有個判斷條件是:sz(之前的size+1)如果大于或等于threshold的話,則調(diào)用rehash方法。
threshold默認是0,在創(chuàng)建ThreadLocalMap時,調(diào)用它的構(gòu)造方法:
ThreadLocalMap(ThreadLocal>?firstKey,?Object?firstValue)?{
????table?=?new?Entry[INITIAL_CAPACITY];
????int?i?=?firstKey.threadLocalHashCode?&?(INITIAL_CAPACITY?-?1);
????table[i]?=?new?Entry(firstKey,?firstValue);
????size?=?1;
????setThreshold(INITIAL_CAPACITY);
}
調(diào)用setThreshold方法給threshold設(shè)置一個值,而這個值INITIAL_CAPACITY是默認的大小16。
private?void?setThreshold(int?len)?{
????threshold?=?len?*?2?/?3;
}
也就是第一次設(shè)置的threshold = 16 * 2 / 3, 取整后的值是:10。
換句話說當sz大于等于10時,就可以考慮擴容了。
rehash代碼如下:
private?void?rehash()?{
????//先嘗試回收一次key為null的值,騰出一些空間
????expungeStaleEntries();
????if?(size?>=?threshold?-?threshold?/?4)
????????resize();
}
在真正擴容之前,先嘗試回收一次key為null的值,騰出一些空間。
如果回收之后的size大于等于threshold的3/4時,才需要真正的擴容。
計算公式如下:
16?*?2?*?4?/?3?*?4?-?16?*?2?/?3?*?4?=?8
也就是說添加數(shù)據(jù)后,新的size大于等于老size的1/2時,才需要擴容。
private?void?resize()?{
????Entry[]?oldTab?=?table;
????int?oldLen?=?oldTab.length;
????//按2倍的大小擴容
????int?newLen?=?oldLen?*?2;
????Entry[]?newTab?=?new?Entry[newLen];
????int?count?=?0;
????for?(int?j?=?0;?j?????????Entry?e?=?oldTab[j];
????????if?(e?!=?null)?{
????????????ThreadLocal>?k?=?e.get();
????????????if?(k?==?null)?{
????????????????e.value?=?null;?//?Help?the?GC
????????????}?else?{
????????????????int?h?=?k.threadLocalHashCode?&?(newLen?-?1);
????????????????while?(newTab[h]?!=?null)
????????????????????h?=?nextIndex(h,?newLen);
????????????????newTab[h]?=?e;
????????????????count++;
????????????}
????????}
????}
????setThreshold(newLen);
????size?=?count;
????table?=?newTab;
}
resize中每次都是按2倍的大小擴容。
擴容的過程如下圖所示:
擴容的關(guān)鍵步驟如下:
老size + 1 = 新size 如果新size大于等于老size的2/3時,需要考慮擴容。 擴容前先嘗試回收一次key為null的值,騰出一些空間。 如果回收之后發(fā)現(xiàn)size還是大于等于老size的1/2時,才需要真正的擴容。 每次都是按2倍的大小擴容。
9. 父子線程如何共享數(shù)據(jù)?
前面介紹的ThreadLocal都是在一個線程中保存和獲取數(shù)據(jù)的。
但在實際工作中,有可能是在父子線程中共享數(shù)據(jù)的。即在父線程中往ThreadLocal設(shè)置了值,在子線程中能夠獲取到。
例如:
public?class?ThreadLocalTest?{
????public?static?void?main(String[]?args)?{
????????ThreadLocal?threadLocal?=?new?ThreadLocal<>();
????????threadLocal.set(6);
????????System.out.println("父線程獲取數(shù)據(jù):"?+?threadLocal.get());
????????new?Thread(()?->?{
????????????System.out.println("子線程獲取數(shù)據(jù):"?+?threadLocal.get());
????????}).start();
????}
}
執(zhí)行結(jié)果:
父線程獲取數(shù)據(jù):6
子線程獲取數(shù)據(jù):null
你會發(fā)現(xiàn),在這種情況下使用ThreadLocal是行不通的。main方法是在主線程中執(zhí)行的,相當于父線程。在main方法中開啟了另外一個線程,相當于子線程。
顯然通過ThreadLocal,無法在父子線程中共享數(shù)據(jù)。
那么,該怎么辦呢?
答:使用InheritableThreadLocal,它是JDK自帶的類,繼承了ThreadLocal類。
修改代碼之后:
public?class?ThreadLocalTest?{
????public?static?void?main(String[]?args)?{
????????InheritableThreadLocal?threadLocal?=?new?InheritableThreadLocal<>();
????????threadLocal.set(6);
????????System.out.println("父線程獲取數(shù)據(jù):"?+?threadLocal.get());
????????new?Thread(()?->?{
????????????System.out.println("子線程獲取數(shù)據(jù):"?+?threadLocal.get());
????????}).start();
????}
}
執(zhí)行結(jié)果:
父線程獲取數(shù)據(jù):6
子線程獲取數(shù)據(jù):6
果然,在換成InheritableThreadLocal之后,在子線程中能夠正常獲取父線程中設(shè)置的值。
其實,在Thread類中除了成員變量threadLocals之外,還有另一個成員變量:inheritableThreadLocals。
Thread類的部分代碼如下:
ThreadLocal.ThreadLocalMap?threadLocals?=?null;
ThreadLocal.ThreadLocalMap?inheritableThreadLocals?=?null;
最關(guān)鍵的一點是,在它的init方法中會將父線程中往ThreadLocal設(shè)置的值,拷貝一份到子線程中。
感興趣的小伙伴,可以找我私聊。或者看看我后面的文章,后面還會有專欄。
10. 線程池中如何共享數(shù)據(jù)?
在真實的業(yè)務(wù)場景中,一般很少用單獨的線程,絕大多數(shù),都是用的線程池。
那么,在線程池中如何共享ThreadLocal對象生成的數(shù)據(jù)呢?
因為涉及到不同的線程,如果直接使用ThreadLocal,顯然是不合適的。
我們應(yīng)該使用InheritableThreadLocal,具體代碼如下:
private?static?void?fun1()?{
????InheritableThreadLocal?threadLocal?=?new?InheritableThreadLocal<>();
????threadLocal.set(6);
????System.out.println("父線程獲取數(shù)據(jù):"?+?threadLocal.get());
????ExecutorService?executorService?=?Executors.newSingleThreadExecutor();
????threadLocal.set(6);
????executorService.submit(()?->?{
????????System.out.println("第一次從線程池中獲取數(shù)據(jù):"?+?threadLocal.get());
????});
????threadLocal.set(7);
????executorService.submit(()?->?{
????????System.out.println("第二次從線程池中獲取數(shù)據(jù):"?+?threadLocal.get());
????});
}
執(zhí)行結(jié)果:
父線程獲取數(shù)據(jù):6
第一次從線程池中獲取數(shù)據(jù):6
第二次從線程池中獲取數(shù)據(jù):6
由于這個例子中使用了單例線程池,固定線程數(shù)是1。
第一次submit任務(wù)的時候,該線程池會自動創(chuàng)建一個線程。因為使用了InheritableThreadLocal,所以創(chuàng)建線程時,會調(diào)用它的init方法,將父線程中的inheritableThreadLocals數(shù)據(jù)復(fù)制到子線程中。所以我們看到,在主線程中將數(shù)據(jù)設(shè)置成6,第一次從線程池中獲取了正確的數(shù)據(jù)6。
之后,在主線程中又將數(shù)據(jù)改成7,但在第二次從線程池中獲取數(shù)據(jù)卻依然是6。
因為第二次submit任務(wù)的時候,線程池中已經(jīng)有一個線程了,就直接拿過來復(fù)用,不會再重新創(chuàng)建線程了。所以不會再調(diào)用線程的init方法,所以第二次其實沒有獲取到最新的數(shù)據(jù)7,還是獲取的老數(shù)據(jù)6。
那么,這該怎么辦呢?
答:使用TransmittableThreadLocal,它并非JDK自帶的類,而是阿里巴巴開源jar包中的類。
可以通過如下pom文件引入該jar包:
<dependency>
???<groupId>com.alibabagroupId>
???<artifactId>transmittable-thread-localartifactId>
???<version>2.11.0version>
???<scope>compilescope>
dependency>
代碼調(diào)整如下:
private?static?void?fun2()?throws?Exception?{
????TransmittableThreadLocal?threadLocal?=?new?TransmittableThreadLocal<>();
????threadLocal.set(6);
????System.out.println("父線程獲取數(shù)據(jù):"?+?threadLocal.get());
????ExecutorService?ttlExecutorService?=?TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(1));
????threadLocal.set(6);
????ttlExecutorService.submit(()?->?{
????????System.out.println("第一次從線程池中獲取數(shù)據(jù):"?+?threadLocal.get());
????});
????threadLocal.set(7);
????ttlExecutorService.submit(()?->?{
????????System.out.println("第二次從線程池中獲取數(shù)據(jù):"?+?threadLocal.get());
????});
}
執(zhí)行結(jié)果:
父線程獲取數(shù)據(jù):6
第一次從線程池中獲取數(shù)據(jù):6
第二次從線程池中獲取數(shù)據(jù):7
我們看到,使用了TransmittableThreadLocal之后,第二次從線程中也能正確獲取最新的數(shù)據(jù)7了。
nice。
如果你仔細觀察這個例子,你可能會發(fā)現(xiàn),代碼中除了使用TransmittableThreadLocal類之外,還使用了TtlExecutors.getTtlExecutorService方法,去創(chuàng)建ExecutorService對象。
這是非常重要的地方,如果沒有這一步,TransmittableThreadLocal在線程池中共享數(shù)據(jù)將不會起作用。
創(chuàng)建ExecutorService對象,底層的submit方法會TtlRunnable或TtlCallable對象。
以TtlRunnable類為例,它實現(xiàn)了Runnable接口,同時還實現(xiàn)了它的run方法:
public?void?run()?{
????Map,?Object>?copied?=?(Map)this.copiedRef.get();
????if?(copied?!=?null?&&?(!this.releaseTtlValueReferenceAfterRun?||?this.copiedRef.compareAndSet(copied,?(Object)null)))?{
????????Map?backup?=?TransmittableThreadLocal.backupAndSetToCopied(copied);
????????try?{
????????????this.runnable.run();
????????}?finally?{
????????????TransmittableThreadLocal.restoreBackup(backup);
????????}
????}?else?{
????????throw?new?IllegalStateException("TTL?value?reference?is?released?after?run!");
????}
}
這段代碼的主要邏輯如下:
把當時的ThreadLocal做個備份,然后將父類的ThreadLocal拷貝過來。 執(zhí)行真正的run方法,可以獲取到父類最新的ThreadLocal數(shù)據(jù)。 從備份的數(shù)據(jù)中,恢復(fù)當時的ThreadLocal數(shù)據(jù)。
11. ThreadLocal有哪些用途?
最后,一起聊聊ThreadLocal有哪些用途?
老實說,使用ThreadLocal的場景挺多的。
下面列舉幾個常見的場景:
在spring事務(wù)中,保證一個線程下,一個事務(wù)的多個操作拿到的是一個Connection。 在hiberate中管理session。 在JDK8之前,為了解決SimpleDateFormat的線程安全問題。 獲取當前登錄用戶上下文。 臨時保存權(quán)限數(shù)據(jù)。 使用MDC保存日志信息。
等等,還有很多業(yè)務(wù)場景,這里就不一一列舉了。
由于篇幅有限,今天的內(nèi)容先分享到這里。希望你看了這篇文章,會有所收獲。
接下來留幾個問題給大家思考一下:
ThreadLocal變量為什么建議要定義成static的? Entry數(shù)組為什么要通過hash算法計算下標,即直線尋址法,而不直接使用下標值? 強引用和弱引用有什么區(qū)別? Entry數(shù)組大小,為什么是2的N次方? 使用InheritableThreadLocal時,如果父線程中重新set值,在子線程中能夠正確的獲取修改后的新值嗎?
????
往 期 推 薦
1、拖動文件就能觸發(fā)7-Zip安全漏洞,波及所有版本
3、一次 SQL 查詢優(yōu)化原理分析:900W+ 數(shù)據(jù),從 17s 到 300ms
點分享
點收藏
點點贊
點在看





