ThreadLocal 是面試過程中非常高頻的一個類,這類的復(fù)雜程度絕對是可以帶出一系列連環(huán)炮的面試轟炸。biu biu biu ~~~~.
一直覺得自己對這個類很了解了,但是直到去看源碼,接二連三的技術(shù)浮出水面(弱引用,避免內(nèi)存溢出的操作,開放地址法解決hash 沖突,各種內(nèi)部類的復(fù)雜的關(guān)系),看到你懷疑人生,直到根據(jù)代碼一步一步的畫圖才最終理解(所以本篇文章會有大量的圖)。 這里也給大家一個啟示,面對復(fù)雜的事情的時候,實(shí)在被問題繞暈了,就畫圖吧,借助圖可以讓問題可視化,便于理解。
WAHTThreadLocal 是一個線程的本地變量,也就意味著這個變量是線程獨(dú)有的,是不能與其他線程共享的,這樣就可以避免資源競爭帶來的多線程的問題,這種解決多線程的安全問題和lock(這里的lock 指通過synchronized 或者Lock 等實(shí)現(xiàn)的鎖) 是有本質(zhì)的區(qū)別的:
lock 的資源是多個線程共享的,所以訪問的時候需要加鎖。ThreadLocal 是每個線程都有一個副本,是不需要加鎖的。lock 是通過時間換空間的做法。ThreadLocal 是典型的通過空間換時間的做法。當(dāng)然他們的使用場景也是不同的,關(guān)鍵看你的資源是需要多線程之間共享的還是單線程內(nèi)部共享的
使用ThreadLocal 的使用是非常簡單的,看下面的代碼
看到這里是不是覺得特別簡單?別高興太早,點(diǎn)進(jìn)去代碼看看,你絕對會懷疑人生
源碼分析在分析源碼之前先畫一下ThreadLocal ,ThreadLocalMap 和Thread 的關(guān)系,如果你對他們的關(guān)系還不了解的話,請看我的另一篇文章BAT面試必考:ThreadLocal ,ThreadLocalMap 和Thread 的關(guān)系
set 方法
createMap 方法只是在第一次設(shè)置值的時候創(chuàng)建一個ThreadLocalMap 賦值給Thread 對象的threadLocals 屬性進(jìn)行綁定,以后就可以直接通過這個屬性獲取到值了。從這里可以看出,為什么說ThreadLocal 是線程本地變量來的了
值真正是放在ThreadLocalMap 中存取的,ThreadLocalMap 內(nèi)部類有一個Entry 類,key是ThreadLocal 對象,value 就是你要存放的值,上面的代碼value 存放的就是hello word。ThreadLocalMap 和HashMap的功能類似,但是實(shí)現(xiàn)上卻有很大的不同:
HashMap 的數(shù)據(jù)結(jié)構(gòu)是數(shù)組+鏈表ThreadLocalMap的數(shù)據(jù)結(jié)構(gòu)僅僅是數(shù)組HashMap 是通過鏈地址法解決hash 沖突的問題ThreadLocalMap 是通過開放地址法來解決hash 沖突的問題HashMap 里面的Entry 內(nèi)部類的引用都是強(qiáng)引用ThreadLocalMap里面的Entry 內(nèi)部類中的key 是弱引用,value 是強(qiáng)引用為什么ThreadLocalMap 采用開放地址法來解決哈希沖突?
jdk 中大多數(shù)的類都是采用了鏈地址法來解決hash 沖突,為什么ThreadLocalMap 采用開放地址法來解決哈希沖突呢?首先我們來看看這兩種不同的方式
鏈地址法
這種方法的基本思想是將所有哈希地址為i的元素構(gòu)成一個稱為同義詞鏈的單鏈表,并將單鏈表的頭指針存在哈希表的第i個單元中,因而查找、插入和刪除主要在同義詞鏈中進(jìn)行。列如對于關(guān)鍵字集合{12,67,56,16,25,37, 22,29,15,47,48,34},我們用前面同樣的12為除數(shù),進(jìn)行除留余數(shù)法:
開放地址法
這種方法的基本思想是一旦發(fā)生了沖突,就去尋找下一個空的散列地址(這非常重要,源碼都是根據(jù)這個特性,必須理解這里才能往下走),只要散列表足夠大,空的散列地址總能找到,并將記錄存入。
比如說,我們的關(guān)鍵字集合為{12,33,4,5,15,25},表長為10。 我們用散列函數(shù)f(key) = key mod l0。 當(dāng)計(jì)算前S個數(shù){12,33,4,5}時,都是沒有沖突的散列地址,直接存入(藍(lán)色代表為空的,可以存放數(shù)據(jù)):
計(jì)算key = 15時,發(fā)現(xiàn)f(15) = 5,此時就與5所在的位置沖突。于是我們應(yīng)用上面的公式f(15) = (f(15)+1) mod 10 =6。于是將15存入下標(biāo)為6的位置。這其實(shí)就是房子被人買了于是買下一間的作法:
鏈地址法和開放地址法的優(yōu)缺點(diǎn)
開放地址法:
容易產(chǎn)生堆積問題,不適于大規(guī)模的數(shù)據(jù)存儲。散列函數(shù)的設(shè)計(jì)對沖突會有很大的影響,插入時可能會出現(xiàn)多次沖突的現(xiàn)象。刪除的元素是多個沖突元素中的一個,需要對后面的元素作處理,實(shí)現(xiàn)較復(fù)雜。鏈地址法:
處理沖突簡單,且無堆積現(xiàn)象,平均查找長度短。鏈表中的結(jié)點(diǎn)是動態(tài)申請的,適合構(gòu)造表不能確定長度的情況。刪除結(jié)點(diǎn)的操作易于實(shí)現(xiàn)。只要簡單地刪去鏈表上相應(yīng)的結(jié)點(diǎn)即可。指針需要額外的空間,故當(dāng)結(jié)點(diǎn)規(guī)模較小時,開放定址法較為節(jié)省空間。ThreadLocalMap 采用開放地址法原因
ThreadLocal 中看到一個屬性 HASH_INCREMENT = 0x61c88647 ,0x61c88647 是一個神奇的數(shù)字,讓哈希碼能均勻的分布在2的N次方的數(shù)組里, 即 Entry[] table,關(guān)于這個神奇的數(shù)字google 有很多解析,這里就不重復(fù)說了ThreadLocal 往往存放的數(shù)據(jù)量不會特別大(而且key 是弱引用又會被垃圾回收,及時讓數(shù)據(jù)量更小),這個時候開放地址法簡單的結(jié)構(gòu)會顯得更省空間,同時數(shù)組的查詢效率也是非常高,加上第一點(diǎn)的保障,沖突概率也低弱引用如果對弱引用不了解的同學(xué),先看下我之前的寫的一篇文章別再找了,一文徹底解析Java 中的弱引用(參考官網(wǎng))系
接下來我們看看ThreadLocalMap 中的存放數(shù)據(jù)的內(nèi)部類Entry 的實(shí)現(xiàn)源碼
我們可以知道Entry 的key 是一個弱引用,也就意味這可能會被垃圾回收器回收掉
threadLocal.get()==null
也就意味著被回收掉了
ThreadLocalMap set 方法
還是拿上面解釋開放地址法解釋的例子來說明下。 比如說,我們的關(guān)鍵字集合為{12,33,4,5,15,25},表長為10。 我們用散列函數(shù)f(key) = key mod l0。 當(dāng)計(jì)算前S個數(shù){12,33,4,5,15,25}時,并且此時key=33,k=5 已經(jīng)過期了(藍(lán)色代表為空的,可以存放數(shù)據(jù),紅色代表key 過期,過期的key為null):
replaceStaleEntry 這個方法
第一個for 循環(huán)是向前遍歷數(shù)據(jù)的,直到遍歷到空的entry 就停止(這個是根據(jù)開放地址的線性探測法),這里的例子就是遍歷到index=1就停止了。向前遍歷的過程同時會找出過期的key,這個時候找到的是下標(biāo)index=3 的為過期,進(jìn)入到
if (e.get() == null) slotToExpunge = i;
注意此時slotToExpunge=3,staleSlot=5
第二個for 循環(huán)是從index=staleSlot開始,向后編列的,找出是否有和當(dāng)前匹配的key,有的話進(jìn)行清理過期的對象和重新設(shè)置當(dāng)前的值。這個例子遍歷到index=6 的時候,匹配到key=15的值,進(jìn)入如下代碼
先進(jìn)行數(shù)據(jù)交換,注意此時slotToExpunge=3,staleSlot=5,i=6。這里就是把5 和6 的位置的元素進(jìn)行交換,并且設(shè)置新的value=new,交換后的圖是這樣的
為什么要交換
這里解釋下為什么交換,我們先來看看如果不交換的話,經(jīng)過設(shè)置值和清理過期對象,會是以下這張圖
這個時候如果我們再一次設(shè)置一個key=15,value=new2 的值,通過f(15)=5,這個時候由于上次index=5是過期對象,被清空了,所以可以存在數(shù)據(jù),那么就直接存放在這里了
你看,這樣整個數(shù)組就存在兩個key=15 的數(shù)據(jù)了,這樣是不允許的,所以一定要交換數(shù)據(jù)expungeStaleEntry
接下來我們詳細(xì)模擬下整個過程 根據(jù)我們的例子,key=5,15,25 都是沖突的,并且k=5的值已經(jīng)過期,經(jīng)過replaceStaleEntry 方法,在進(jìn)入expungeStaleEntry 方法之前,數(shù)據(jù)結(jié)構(gòu)是這樣的
此時傳進(jìn)來的參數(shù)staleSlot=6,
這個時候會把index=6設(shè)置為null,數(shù)據(jù)結(jié)構(gòu)變成下面的情況
接下來我們會遍歷到i=7,經(jīng)過int h = k.threadLocalHashCode & (len - 1) (實(shí)際上對應(yīng)我們的舉例的函數(shù)int h= f(25)); 得到的h=5,而25實(shí)際存放在index=7 的位置上,這個時候我們需要從h=5的位置上重新開始編列,直到遇到空的entry 為止
這個時候h=6,并把k=25 的值移到index=6 的位置上,同時設(shè)置index=7 為空,如下圖
其實(shí)目的跟replaceStaleEntry 交換位置的原理是一樣的,為了防止由于回收掉中間那個沖突的值,導(dǎo)致后面沖突的值沒辦法找到(因?yàn)閑==null 就跳出循環(huán)了)
ThreadLocal 內(nèi)存溢出問題通過上面的分析,我們知道expungeStaleEntry() 方法是幫助垃圾回收的,根據(jù)源碼,我們可以發(fā)現(xiàn) get 和set 方法都可能觸發(fā)清理方法expungeStaleEntry(),所以正常情況下是不會有內(nèi)存溢出的 但是如果我們沒有調(diào)用get 和set 的時候就會可能面臨著內(nèi)存溢出,養(yǎng)成好習(xí)慣不再使用的時候調(diào)用remove(),加快垃圾回收,避免內(nèi)存溢出
退一步說,就算我們沒有調(diào)用get 和set 和remove 方法,線程結(jié)束的時候,也就沒有強(qiáng)引用再指向ThreadLocal 中的ThreadLocalMap了,這樣ThreadLocalMap 和里面的元素也會被回收掉,但是有一種危險(xiǎn)是,如果線程是線程池的, 在線程執(zhí)行完代碼的時候并沒有結(jié)束,只是歸還給線程池,這個時候ThreadLocalMap 和里面的元素是不會回收掉的
來源:掘金
鏈接:https://juejin.im/post/5d8b2bde51882509372faa7c