ThreadLocal奪命11連問(wèn)
前言
前一段時(shí)間,有同事使用ThreadLocal踩坑了,正好引起了我的興趣。
所以近期,我抽空把ThreadLocal的源碼再研究了一下,越看越有意思,發(fā)現(xiàn)里面的東西還真不少。
我把精華濃縮了一下,匯集成了下面11個(gè)問(wèn)題,看看你能頂住第幾個(gè)?
1. 為什么要用ThreadLocal?
并發(fā)編程是一項(xiàng)非常重要的技術(shù),它讓我們的程序變得更加高效。
但在并發(fā)的場(chǎng)景中,如果有多個(gè)線程同時(shí)修改公共變量,可能會(huì)出現(xiàn)線程安全問(wèn)題,即該變量最終結(jié)果可能出現(xiàn)異常。
為了解決線程安全問(wèn)題,JDK出現(xiàn)了很多技術(shù)手段,比如:使用synchronized或Lock,給訪問(wèn)公共資源的代碼上鎖,保證了代碼的原子性。
但在高并發(fā)的場(chǎng)景中,如果多個(gè)線程同時(shí)競(jìng)爭(zhēng)一把鎖,這時(shí)會(huì)存在大量的鎖等待,可能會(huì)浪費(fèi)很多時(shí)間,讓系統(tǒng)的響應(yīng)時(shí)間一下子變慢。
因此,JDK還提供了另外一種用空間換時(shí)間的新思路:ThreadLocal。
它的核心思想是:共享變量在每個(gè)線程都有一個(gè)副本,每個(gè)線程操作的都是自己的副本,對(duì)另外的線程沒(méi)有影響。
例如:
@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的底層實(shí)現(xiàn)原理,我們不得不扒一下源碼。
ThreadLocal的內(nèi)部有一個(gè)靜態(tài)的內(nèi)部類叫:ThreadLocalMap。
public?class?ThreadLocal<T>?{
?????...
?????public?T?get()?{
????????//獲取當(dāng)前線程
????????Thread?t?=?Thread.currentThread();
????????//獲取當(dāng)前線程的成員變量ThreadLocalMap對(duì)象
????????ThreadLocalMap?map?=?getMap(t);
????????if?(map?!=?null)?{
????????????//根據(jù)threadLocal對(duì)象從map中獲取Entry對(duì)象
????????????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();
????????//獲取當(dāng)前線程
????????Thread?t?=?Thread.currentThread();
????????//獲取當(dāng)前線程的成員變量ThreadLocalMap對(duì)象
????????ThreadLocalMap?map?=?getMap(t);
????????//如果map不為空
????????if?(map?!=?null)
????????????//將初始值設(shè)置到map中,key是this,即threadLocal對(duì)象,value是初始值
????????????map.set(this,?value);
????????else
???????????//如果map為空,則需要?jiǎng)?chuàng)建新的map對(duì)象
????????????createMap(t,?value);
????????return?value;
????}
????
????public?void?set(T?value)?{
????????//獲取當(dāng)前線程
????????Thread?t?=?Thread.currentThread();
????????//獲取當(dāng)前線程的成員變量ThreadLocalMap對(duì)象
????????ThreadLocalMap?map?=?getMap(t);
????????//如果map不為空
????????if?(map?!=?null)
????????????//將值設(shè)置到map中,key是this,即threadLocal對(duì)象,value是傳入的value值
????????????map.set(this,?value);
????????else
???????????//如果map為空,則需要?jiǎng)?chuàng)建新的map對(duì)象
????????????createMap(t,?value);
????}
????
?????static?class?ThreadLocalMap?{
????????...
?????}
?????...
}
ThreadLocal的get方法、set方法和setInitialValue方法,其實(shí)最終操作的都是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里面包含一個(gè)靜態(tài)的內(nèi)部類Entry,該類繼承于WeakReference類,說(shuō)明Entry是一個(gè)弱引用。
ThreadLocalMap內(nèi)部還包含了一個(gè)Entry數(shù)組,其中:Entry = ThreadLocal + value。
而ThreadLocalMap被定義成了Thread類的成員變量。
public?class?Thread?implements?Runnable?{
????...
????ThreadLocal.ThreadLocalMap?threadLocals?=?null;
}
下面用一張圖從宏觀上,認(rèn)識(shí)一下ThreadLocal的整體結(jié)構(gòu):
從上圖中看出,在每個(gè)Thread類中,都有一個(gè)ThreadLocalMap的成員變量,該變量包含了一個(gè)Entry數(shù)組,該數(shù)組真正保存了ThreadLocal類set的數(shù)據(jù)。
Entry是由threadLocal和value組成,其中threadLocal對(duì)象是弱引用,在GC的時(shí)候,會(huì)被自動(dòng)回收。而value就是ThreadLocal類set的數(shù)據(jù)。
下面用一張圖總結(jié)一下引用關(guān)系:
上圖中除了Entry的key對(duì)ThreadLocal對(duì)象是弱引用,其他的引用都是強(qiáng)引用。
需要特別說(shuō)明的是,上圖中ThreadLocal對(duì)象我畫到了堆上,其實(shí)在實(shí)際的業(yè)務(wù)場(chǎng)景中不一定在堆上。因?yàn)槿绻鸗hreadLocal被定義成了static的,ThreadLocal的對(duì)象是類共用的,可能出現(xiàn)在方法區(qū)。
3. 為什么用ThreadLocal做key?
不知道你有沒(méi)有思考過(guò)這樣一個(gè)問(wèn)題:ThreadLocalMap為什么要用ThreadLocal做key,而不是用Thread做key?
如果在你的應(yīng)用中,一個(gè)線程中只使用了一個(gè)ThreadLocal對(duì)象,那么使用Thread做key也未嘗不可。
@Service
public?class?ThreadLocalService?{
????private?static?final?ThreadLocal?threadLocal?=?new?ThreadLocal<>();
}????
但實(shí)際情況中,你的應(yīng)用,一個(gè)線程中很有可能不只使用了一個(gè)ThreadLocal對(duì)象。這時(shí)使用Thread做key不就出有問(wèn)題?
@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時(shí),你的代碼中定義了3個(gè)ThreadLocal對(duì)象,那么,通過(guò)Thread對(duì)象,它怎么知道要獲取哪個(gè)ThreadLocal對(duì)象呢?
如下圖所示:
因此,不能使用Thread做key,而應(yīng)該改成用ThreadLocal對(duì)象做key,這樣才能通過(guò)具體ThreadLocal對(duì)象的get方法,輕松獲取到你想要的ThreadLocal對(duì)象。
如下圖所示:
4. Entry的key為什么設(shè)計(jì)成弱引用?
前面說(shuō)過(guò),Entry的key,傳入的是ThreadLocal對(duì)象,使用了WeakReference對(duì)象,即被設(shè)計(jì)成了弱引用。
那么,為什么要這樣設(shè)計(jì)呢?
假如key對(duì)ThreadLocal對(duì)象的弱引用,改為強(qiáng)引用。
我們都知道ThreadLocal變量對(duì)ThreadLocal對(duì)象是有強(qiáng)引用存在的。
即使ThreadLocal變量生命周期完了,設(shè)置成null了,但由于key對(duì)ThreadLocal還是強(qiáng)引用。
此時(shí),如果執(zhí)行該代碼的線程使用了線程池,一直長(zhǎng)期存在,不會(huì)被銷毀。
就會(huì)存在這樣的強(qiáng)引用鏈:Thread變量 -> Thread對(duì)象 -> ThreadLocalMap -> Entry -> key -> ThreadLocal對(duì)象。
那么,ThreadLocal對(duì)象和ThreadLocalMap都將不會(huì)被GC回收,于是產(chǎn)生了內(nèi)存泄露問(wèn)題。
為了解決這個(gè)問(wèn)題,JDK的開(kāi)發(fā)者們把Entry的key設(shè)計(jì)成了弱引用。
弱引用的對(duì)象,在GC做垃圾清理的時(shí)候,就會(huì)被自動(dòng)回收了。
如果key是弱引用,當(dāng)ThreadLocal變量指向null之后,在GC做垃圾清理的時(shí)候,key會(huì)被自動(dòng)回收,其值也被設(shè)置成null。
如下圖所示:
接下來(lái),最關(guān)鍵的地方來(lái)了。
由于當(dāng)前的ThreadLocal變量已經(jīng)被指向null了,但如果直接調(diào)用它的get、set或remove方法,很顯然會(huì)出現(xiàn)空指針異常。因?yàn)樗纳呀?jīng)結(jié)束了,再調(diào)用它的方法也沒(méi)啥意義。
此時(shí),如果系統(tǒng)中還定義了另外一個(gè)ThreadLocal變量b,調(diào)用了它的get、set或remove,三個(gè)方法中的任何一個(gè)方法,都會(huì)自動(dòng)觸發(fā)清理機(jī)制,將key為null的value值清空。
如果key和value都是null,那么Entry對(duì)象會(huì)被GC回收。如果所有的Entry對(duì)象都被回收了,ThreadLocalMap也會(huì)被回收了。
這樣就能最大程度的解決內(nèi)存泄露問(wèn)題。
需要特別注意的地方是:
key為null的條件是,ThreadLocal變量指向 null,并且key是弱引用。如果ThreadLocal變量沒(méi)有斷開(kāi)對(duì)ThreadLocal的強(qiáng)引用,即ThreadLocal變量沒(méi)有指向null,GC就貿(mào)然的把弱引用的key回收了,不就會(huì)影響正常用戶的使用?如果當(dāng)前ThreadLocal變量指向 null了,并且key也為null了,但如果沒(méi)有其他ThreadLocal變量觸發(fā)get、set或remove方法,也會(huì)造成內(nèi)存泄露。
下面看看弱引用的例子:
public?static?void?main(String[]?args)?{
????WeakReference打印結(jié)果:
java.lang.Object@1ef7fe8e
null
傳入WeakReference構(gòu)造方法的是直接new處理的對(duì)象,沒(méi)有其他引用,在調(diào)用gc方法后,弱引用對(duì)象會(huì)被自動(dòng)回收。
但如果出現(xiàn)下面這種情況:
public?static?void?main(String[]?args)?{
????Object?object?=?new?Object();
????WeakReference執(zhí)行結(jié)果:
java.lang.Object@1ef7fe8e
java.lang.Object@1ef7fe8e
先定義了一個(gè)強(qiáng)引用object對(duì)象,在WeakReference構(gòu)造方法中將object對(duì)象的引用作為參數(shù)傳入。這時(shí),調(diào)用gc后,弱引用對(duì)象不會(huì)被自動(dòng)回收。
我們的Entry對(duì)象中的key不就是第二種情況嗎?在Entry構(gòu)造方法中傳入的是ThreadLocal對(duì)象的引用。
如果將object強(qiáng)引用設(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之后,弱引用能夠被正常回收。
由此可見(jiàn),如果強(qiáng)引用和弱引用同時(shí)關(guān)聯(lián)一個(gè)對(duì)象,那么這個(gè)對(duì)象是不會(huì)被GC回收。也就是說(shuō)這種情況下Entry的key,一直都不會(huì)為null,除非強(qiáng)引用主動(dòng)斷開(kāi)關(guān)聯(lián)。
此外,你可能還會(huì)問(wèn)這樣一個(gè)問(wèn)題:Entry的value為什么不設(shè)計(jì)成弱引用?
答:Entry的value假如只是被Entry引用,有可能沒(méi)被業(yè)務(wù)系統(tǒng)中的其他地方引用。如果將value改成了弱引用,被GC貿(mào)然回收了(數(shù)據(jù)突然沒(méi)了),可能會(huì)導(dǎo)致業(yè)務(wù)系統(tǒng)出現(xiàn)異常。
而相比之下,Entry的key,管理的地方就非常明確了。
這就是Entry的key被設(shè)計(jì)成弱引用,而value沒(méi)被設(shè)計(jì)成弱引用的原因。
5. ThreadLocal真的會(huì)導(dǎo)致內(nèi)存泄露?
通過(guò)上面的Entry對(duì)象中的key設(shè)置成弱引用,并且使用get、set或remove方法清理key為null的value值,就能徹底解決內(nèi)存泄露問(wèn)題?
答案是否定的。
如下圖所示:
假如ThreadLocalMap中存在很多key為null的Entry,但后面的程序,一直都沒(méi)有調(diào)用過(guò)有效的ThreadLocal的get、set或remove方法。
那么,Entry的value值一直都沒(méi)被清空。
所以會(huì)存在這樣一條強(qiáng)引用鏈:Thread變量 -> Thread對(duì)象 -> ThreadLocalMap -> Entry -> value -> Object。
其結(jié)果就是:Entry和ThreadLocalMap將會(huì)長(zhǎng)期存在下去,會(huì)導(dǎo)致內(nèi)存泄露。
6. 如何解決內(nèi)存泄露問(wèn)題?
前面說(shuō)過(guò)的ThreadLocal還是會(huì)導(dǎo)致內(nèi)存泄露的問(wèn)題,我們有沒(méi)有解決辦法呢?
答:有辦法,調(diào)用ThreadLocal對(duì)象的remove方法。
不是在一開(kāi)始就調(diào)用remove方法,而是在使用完ThreadLocal對(duì)象之后。列如:
先創(chuàng)建一個(gè)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方法清理沒(méi)用的數(shù)據(jù)。如果業(yè)務(wù)代碼出現(xiàn)異常,也能及時(shí)清理沒(méi)用的數(shù)據(jù)。
remove方法中會(huì)把Entry中的key和value都設(shè)置成null,這樣就能被GC及時(shí)回收,無(wú)需觸發(fā)額外的清理機(jī)制,所以它能解決內(nèi)存泄露問(wèn)題。
7. ThreadLocal是如何定位數(shù)據(jù)的?
前面說(shuō)過(guò)ThreadLocalMap對(duì)象底層是用Entry數(shù)組保存數(shù)據(jù)的。
那么問(wèn)題來(lái)了,ThreadLocal是如何定位Entry數(shù)組數(shù)據(jù)的?
在ThreadLocal的get、set、remove方法中都有這樣一行代碼:
int?i?=?key.threadLocalHashCode?&?(len-1);
通過(guò)key的hashCode值,與數(shù)組的長(zhǎng)度減1。其中key就是ThreadLocal對(duì)象,與數(shù)組的長(zhǎng)度減1,相當(dāng)于除以數(shù)組的長(zhǎng)度減1,然后取模。
這是一種hash算法。
接下來(lái)給大家舉個(gè)例子:假設(shè)len=16,key.threadLocalHashCode=31,
于是: int i = 31 & 15 = 15
相當(dāng)于:int i = 31 % 16 = 15
計(jì)算的結(jié)果是一樣的,但是使用與運(yùn)算效率跟高一些。
為什么與運(yùn)算效率更高?
答:因?yàn)門hreadLocal的初始大小是16,每次都是按2倍擴(kuò)容,數(shù)組的大小其實(shí)一直都是2的n次方。這種數(shù)據(jù)有個(gè)規(guī)律就是高位是0,低位都是1。在做與運(yùn)算時(shí),可以不用考慮高位,因?yàn)榕c運(yùn)算的結(jié)果必定是0。只需考慮低位的與運(yùn)算,所以效率更高。
如果使用hash算法定位具體位置的話,就可能會(huì)出現(xiàn)hash沖突的情況,即兩個(gè)不同的hashCode取模后的值相同。
ThreadLocal是如何解決hash沖突的呢?
我們看看getEntry是怎么做的:
private?Entry?getEntry(ThreadLocal>?key)?{
????//通過(guò)hash算法獲取下標(biāo)值
????int?i?=?key.threadLocalHashCode?&?(table.length?-?1);
????Entry?e?=?table[i];
????//如果下標(biāo)位置上的key正好是我們所需要尋找的key
????if?(e?!=?null?&&?e.get()?==?key)
????????//說(shuō)明找到數(shù)據(jù)了,直接返回
????????return?e;
????else
????????//說(shuō)明出現(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對(duì)象如果不為空,則一直循環(huán)
????while?(e?!=?null)?{
????????ThreadLocal>?k?=?e.get();
????????//如果當(dāng)前Entry的key正好是我們所需要尋找的key
????????if?(k?==?key)
????????????//說(shuō)明這次真的找到數(shù)據(jù)了
????????????return?e;
????????if?(k?==?null)
????????????//如果key為空,則清理臟數(shù)據(jù)
????????????expungeStaleEntry(i);
????????else
????????????//如果還是沒(méi)找到數(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);
}
當(dāng)通過(guò)hash算法計(jì)算出的下標(biāo)小于數(shù)組大小,則將下標(biāo)值加1。否則,即下標(biāo)大于等于數(shù)組大小,下標(biāo)變成0了。下標(biāo)變成0之后,則循環(huán)一次,下標(biāo)又變成1。。。
尋找的大致過(guò)程如下圖所示:
如果找到最后一個(gè),還是沒(méi)有找到,則再?gòu)念^開(kāi)始找。
不知道你有沒(méi)有發(fā)現(xiàn),它構(gòu)成了一個(gè):環(huán)形。
ThreadLocal從數(shù)組中找數(shù)據(jù)的過(guò)程大致是這樣的:
通過(guò)key的hashCode取余計(jì)算出一個(gè)下標(biāo)。 通過(guò)下標(biāo),在數(shù)組中定位具體Entry,如果key正好是我們所需要的key,說(shuō)明找到了,則直接返回?cái)?shù)據(jù)。 如果第2步?jīng)]有找到我們想要的數(shù)據(jù),則從數(shù)組的下標(biāo)位置,繼續(xù)往后面找。 如果第3步中找key的正好是我們所需要的key,說(shuō)明找到了,則直接返回?cái)?shù)據(jù)。 如果還是沒(méi)有找到數(shù)據(jù),再繼續(xù)往后面找。如果找到最后一個(gè)位置,還是沒(méi)有找到數(shù)據(jù),則再?gòu)念^,即下標(biāo)為0的位置,繼續(xù)從前往后找數(shù)據(jù)。 直到找到第一個(gè)Entry為空為止。
8. ThreadLocal是如何擴(kuò)容的?
從上面得知,ThreadLocal的初始大小是16。那么問(wèn)題來(lái)了,ThreadLocal是如何擴(kuò)容的?
在set方法中會(huì)調(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();
}
注意一下,其中有個(gè)判斷條件是:sz(之前的size+1)如果大于或等于threshold的話,則調(diào)用rehash方法。
threshold默認(rèn)是0,在創(chuàng)建ThreadLocalMap時(shí),調(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è)置一個(gè)值,而這個(gè)值INITIAL_CAPACITY是默認(rèn)的大小16。
private?void?setThreshold(int?len)?{
????threshold?=?len?*?2?/?3;
}
也就是第一次設(shè)置的threshold = 16 * 2 / 3, 取整后的值是:10。
換句話說(shuō)當(dāng)sz大于等于10時(shí),就可以考慮擴(kuò)容了。
rehash代碼如下:
private?void?rehash()?{
????//先嘗試回收一次key為null的值,騰出一些空間
????expungeStaleEntries();
????if?(size?>=?threshold?-?threshold?/?4)
????????resize();
}
在真正擴(kuò)容之前,先嘗試回收一次key為null的值,騰出一些空間。
如果回收之后的size大于等于threshold的3/4時(shí),才需要真正的擴(kuò)容。
計(jì)算公式如下:
16?*?2?*?4?/?3?*?4?-?16?*?2?/?3?*?4?=?8
也就是說(shuō)添加數(shù)據(jù)后,新的size大于等于老size的1/2時(shí),才需要擴(kuò)容。
private?void?resize()?{
????Entry[]?oldTab?=?table;
????int?oldLen?=?oldTab.length;
????//按2倍的大小擴(kuò)容
????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倍的大小擴(kuò)容。
擴(kuò)容的過(guò)程如下圖所示:
擴(kuò)容的關(guān)鍵步驟如下:
老size + 1 = 新size 如果新size大于等于老size的2/3時(shí),需要考慮擴(kuò)容。 擴(kuò)容前先嘗試回收一次key為null的值,騰出一些空間。 如果回收之后發(fā)現(xiàn)size還是大于等于老size的1/2時(shí),才需要真正的擴(kuò)容。 每次都是按2倍的大小擴(kuò)容。
9. 父子線程如何共享數(shù)據(jù)?
前面介紹的ThreadLocal都是在一個(gè)線程中保存和獲取數(shù)據(jù)的。
但在實(shí)際工作中,有可能是在父子線程中共享數(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
你會(huì)發(fā)現(xiàn),在這種情況下使用ThreadLocal是行不通的。main方法是在主線程中執(zhí)行的,相當(dāng)于父線程。在main方法中開(kāi)啟了另外一個(gè)線程,相當(dāng)于子線程。
顯然通過(guò)ThreadLocal,無(wú)法在父子線程中共享數(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è)置的值。
其實(shí),在Thread類中除了成員變量threadLocals之外,還有另一個(gè)成員變量:inheritableThreadLocals。
Thread類的部分代碼如下:
ThreadLocal.ThreadLocalMap?threadLocals?=?null;
ThreadLocal.ThreadLocalMap?inheritableThreadLocals?=?null;
最關(guān)鍵的一點(diǎn)是,在它的init方法中會(huì)將父線程中往ThreadLocal設(shè)置的值,拷貝一份到子線程中。
感興趣的小伙伴,可以找我私聊。或者看看我后面的文章,后面還會(huì)有專欄。
10. 線程池中如何共享數(shù)據(jù)?
在真實(shí)的業(yè)務(wù)場(chǎng)景中,一般很少用單獨(dú)的線程,絕大多數(shù),都是用的線程池。
那么,在線程池中如何共享ThreadLocal對(duì)象生成的數(shù)據(jù)呢?
因?yàn)樯婕暗讲煌木€程,如果直接使用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
由于這個(gè)例子中使用了單例線程池,固定線程數(shù)是1。
第一次submit任務(wù)的時(shí)候,該線程池會(huì)自動(dòng)創(chuàng)建一個(gè)線程。因?yàn)槭褂昧薎nheritableThreadLocal,所以創(chuàng)建線程時(shí),會(huì)調(diào)用它的init方法,將父線程中的inheritableThreadLocals數(shù)據(jù)復(fù)制到子線程中。所以我們看到,在主線程中將數(shù)據(jù)設(shè)置成6,第一次從線程池中獲取了正確的數(shù)據(jù)6。
之后,在主線程中又將數(shù)據(jù)改成7,但在第二次從線程池中獲取數(shù)據(jù)卻依然是6。
因?yàn)榈诙蝧ubmit任務(wù)的時(shí)候,線程池中已經(jīng)有一個(gè)線程了,就直接拿過(guò)來(lái)復(fù)用,不會(huì)再重新創(chuàng)建線程了。所以不會(huì)再調(diào)用線程的init方法,所以第二次其實(shí)沒(méi)有獲取到最新的數(shù)據(jù)7,還是獲取的老數(shù)據(jù)6。
那么,這該怎么辦呢?
答:使用TransmittableThreadLocal,它并非JDK自帶的類,而是阿里巴巴開(kāi)源jar包中的類。
可以通過(guò)如下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。
如果你仔細(xì)觀察這個(gè)例子,你可能會(huì)發(fā)現(xiàn),代碼中除了使用TransmittableThreadLocal類之外,還使用了TtlExecutors.getTtlExecutorService方法,去創(chuàng)建ExecutorService對(duì)象。
這是非常重要的地方,如果沒(méi)有這一步,TransmittableThreadLocal在線程池中共享數(shù)據(jù)將不會(huì)起作用。
創(chuàng)建ExecutorService對(duì)象,底層的submit方法會(huì)TtlRunnable或TtlCallable對(duì)象。
以TtlRunnable類為例,它實(shí)現(xiàn)了Runnable接口,同時(shí)還實(shí)現(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!");
????}
}
這段代碼的主要邏輯如下:
把當(dāng)時(shí)的ThreadLocal做個(gè)備份,然后將父類的ThreadLocal拷貝過(guò)來(lái)。 執(zhí)行真正的run方法,可以獲取到父類最新的ThreadLocal數(shù)據(jù)。 從備份的數(shù)據(jù)中,恢復(fù)當(dāng)時(shí)的ThreadLocal數(shù)據(jù)。
11. ThreadLocal有哪些用途?
最后,一起聊聊ThreadLocal有哪些用途?
老實(shí)說(shuō),使用ThreadLocal的場(chǎng)景挺多的。
下面列舉幾個(gè)常見(jiàn)的場(chǎng)景:
在spring事務(wù)中,保證一個(gè)線程下,一個(gè)事務(wù)的多個(gè)操作拿到的是一個(gè)Connection。 在hiberate中管理session。 在JDK8之前,為了解決SimpleDateFormat的線程安全問(wèn)題。 獲取當(dāng)前登錄用戶上下文。 臨時(shí)保存權(quán)限數(shù)據(jù)。 使用MDC保存日志信息。
等等,還有很多業(yè)務(wù)場(chǎng)景,這里就不一一列舉了。
由于篇幅有限,今天的內(nèi)容先分享到這里。希望你看了這篇文章,會(huì)有所收獲。
接下來(lái)留幾個(gè)問(wèn)題給大家思考一下:
ThreadLocal變量為什么建議要定義成static的? Entry數(shù)組為什么要通過(guò)hash算法計(jì)算下標(biāo),即直線尋址法,而不直接使用下標(biāo)值? 強(qiáng)引用和弱引用有什么區(qū)別? Entry數(shù)組大小,為什么是2的N次方? 使用InheritableThreadLocal時(shí),如果父線程中重新set值,在子線程中能夠正確的獲取修改后的新值嗎?
敬請(qǐng)期待我的下一篇文章,謝謝。

往期推薦

下個(gè)十年高性能 JSON 庫(kù)來(lái)了:fastjson2!

一文詳解讀寫鎖

梳理50道經(jīng)典計(jì)算機(jī)網(wǎng)絡(luò)面試題

