一文摸透ThreadLocal

ThreadLocal是一個(gè)線程內(nèi)部的數(shù)據(jù)存儲(chǔ)類(lèi)奢赂,它用來(lái)存儲(chǔ)那種---以線程為作用域并且不同線程具有不同的數(shù)據(jù)副本的這類(lèi)數(shù)據(jù)漓摩。

如果沒(méi)有這個(gè)東西惑艇,如果我們要實(shí)現(xiàn)線程隔離的一些數(shù)據(jù)副本的存儲(chǔ)寓免,該怎么做苞俘?我們會(huì)創(chuàng)建一個(gè)當(dāng)前進(jìn)程下的盹沈,全局哈希表龄章。這個(gè)哈希表對(duì)所有線程可見(jiàn)吃谣。但是這樣做會(huì)有三個(gè)問(wèn)題:

  • 需要為每個(gè)存儲(chǔ)的對(duì)象都創(chuàng)建一個(gè)哈希表,比如面向Looper的哈希表和面向某個(gè)String對(duì)象的哈希表做裙。亦或是只用一個(gè)哈希表岗憋,但是哈希表里的桶,需要預(yù)估存儲(chǔ)的數(shù)據(jù)量和數(shù)據(jù)類(lèi)型锚贱,然后采用相應(yīng)的存儲(chǔ)結(jié)構(gòu)仔戈。
  • 既然是當(dāng)前虛擬機(jī)內(nèi)所有的線程可見(jiàn),那就需要處理并發(fā)讀寫(xiě)的問(wèn)題拧廊,涉及到加鎖监徘,容易出錯(cuò)。

于是相比較之下吧碾,還是ThreadLocal的方案更優(yōu)雅凰盔。

在這個(gè)基礎(chǔ)上還可以解決復(fù)雜邏輯下的對(duì)象傳遞,比如傳遞監(jiān)聽(tīng)器倦春。

否則只能直接通過(guò)參數(shù)的形式傳遞監(jiān)聽(tīng)器或者把監(jiān)聽(tīng)器定義成靜態(tài)變量户敬。前者在調(diào)用棧很深的時(shí)候無(wú)法接受落剪,后者不具備可擴(kuò)展性,可能會(huì)有很多靜態(tài)監(jiān)聽(tīng)器對(duì)象尿庐。

它的大致結(jié)構(gòu)是如下圖這樣的:


image.png

每個(gè)線程Thread會(huì)持有一個(gè)ThreadLocalMap對(duì)象忠怖,這個(gè)對(duì)象是一個(gè)長(zhǎng)度為16的數(shù)組,數(shù)組里存放我們剛剛說(shuō)的數(shù)據(jù)副本抄瑟。這個(gè)數(shù)組的桶里是一個(gè)K,V對(duì)凡泣,key是我們創(chuàng)建的ThreadLocal對(duì)象本身,value就是真正存儲(chǔ)的數(shù)據(jù)皮假。也就是說(shuō)每個(gè)線程能存放的數(shù)據(jù)量是16個(gè)對(duì)象问麸。能不能擴(kuò)展呢,不可以手動(dòng)擴(kuò)展钞翔,至少在android-28的源碼里严卖,是沒(méi)有擴(kuò)展的入口的。但是在數(shù)據(jù)插入超過(guò)裝載因子的情況下布轿,會(huì)進(jìn)行擴(kuò)容哮笆。

至此這個(gè)TL的原理就講完了,接下來(lái)會(huì)涉及到android平臺(tái)相關(guān)的一些代碼細(xì)節(jié)來(lái)證實(shí)汰扭,不是必看內(nèi)容稠肘。

它是如何通過(guò)這樣簡(jiǎn)單的get和set,完成這種線程間相互隔離的數(shù)據(jù)存儲(chǔ)方案萝毛?


image.png

先看set方法:


image.png

先拿到當(dāng)前的線程t----然后根據(jù)當(dāng)前線程t來(lái)得到當(dāng)前線程的ThreadLocalMap项阴,如果沒(méi)有就創(chuàng)建。有的話笆包,就調(diào)用set方法环揽,這個(gè)this就是我們創(chuàng)建的threadlocal實(shí)例,value就是具體的數(shù)據(jù)庵佣。

這里值得一提的是歉胶,這個(gè)K,V對(duì)巴粪,里的key也就是ThreadLocal的實(shí)例通今,是被弱引用的。


image.png

目的就是在threadlocal被回收的時(shí)候肛根,能清除掉數(shù)組里的過(guò)期槽位(所謂過(guò)期槽位就是key為null的槽)辫塌。

這個(gè)ThreadLocalMap是線程Thread持有的一個(gè)成員變量。


image.png

由此對(duì)應(yīng)到上面那張我手畫(huà)的圖派哲,每個(gè)Thread持有一個(gè)ThreadLocalMap臼氨。

看下map的創(chuàng)建:


image.png

它只有一個(gè)構(gòu)造函數(shù),且沒(méi)有提供設(shè)置初始化數(shù)組大小的入口狮辽,所以我說(shuō)這個(gè)16的初始值沒(méi)法手動(dòng)修改一也。但是如果set的數(shù)據(jù)超過(guò)裝載因子巢寡,就會(huì)進(jìn)行rehash。

image.png

這個(gè)threshold的值是size的三分之二:


image.png

然后我們?cè)倏聪聄ehash:


image.png
image.png

如上圖椰苟,超過(guò)裝載因子以后會(huì)擴(kuò)容成原來(lái)的2倍大抑月。即新建一個(gè)兩倍大的數(shù)組,然后把原始拷貝過(guò)去舆蝴,這個(gè)過(guò)程和arraylist的擴(kuò)容操作類(lèi)似谦絮,其實(shí)數(shù)組這中結(jié)構(gòu),擴(kuò)容的辦法都是這樣的洁仗,先復(fù)制层皱,再拷貝。

回到剛剛的set方法赠潦,補(bǔ)充一句叫胖,是先對(duì)key進(jìn)行hash,之后計(jì)算出理論的槽位她奥,然后嘗試放入瓮增,槽位為空或者key為null直接覆蓋,否則就嘗試下一個(gè)index(即 用線性探測(cè)法解決哈希沖突)哩俭。

在ThreadLocal的使用過(guò)程中绷跑,可能出現(xiàn)內(nèi)存泄漏和線程不安全的情況。

  • 內(nèi)存泄漏

    前面說(shuō)過(guò)了凡资,kv對(duì)中的key是用弱引用持有的ThreadLocal的實(shí)例砸捏,當(dāng)key被回收以后,value會(huì)在下次set的時(shí)候被當(dāng)做過(guò)期的槽位清空隙赁。
    但是這個(gè)不夠及時(shí)垦藏,如果沒(méi)有下個(gè)set操作的到來(lái),線程也遲遲不結(jié)束鸳谜,就會(huì)存在對(duì)value的強(qiáng)引用因?yàn)関alue不會(huì)被訪問(wèn)了但是釋放不掉導(dǎo)致內(nèi)存泄漏膝藕。只能等當(dāng)前thread結(jié)束以后,強(qiáng)引用被斷開(kāi)咐扭,Current Thread、Map value才會(huì)全部被GC回收滑废。
    最好的辦法是在不用這個(gè)value以后蝗肪,手動(dòng)調(diào)用remove主動(dòng)清空槽位。
    這種情況下如何和線程池配合使用蠕趁,需要格外小心薛闪,因?yàn)榫€程池里的線程一直不斷的重復(fù)運(yùn)行,可能造成value堆積俺陋,更需要及時(shí)調(diào)用remove了豁延。

  • 線程不安全

    這個(gè)線程不安全昙篙,翻譯過(guò)來(lái)就是,能在A線程里的ThreadLocal更改B線程里的ThreadLocal诱咏。
    應(yīng)該避免這種情況苔可,即不同線程里的ThreadLocal持有同一個(gè)對(duì)象(靜態(tài)對(duì)象)(static修飾的類(lèi)在JVM中只保存一個(gè)實(shí)例對(duì)象)。

總結(jié)提煉一下袋狞,ThreadLocal的意義是什么焚辅?回顧文章開(kāi)頭我對(duì)比哪個(gè)全局哈希表的解決方案。其實(shí)ThreadLocal是解決線程安全的一個(gè)好辦法苟鸯,為每個(gè)線程提供了獨(dú)立的變量副本解決了線程共享變量并發(fā)訪問(wèn)的問(wèn)題同蜻。這個(gè)并發(fā)訪問(wèn)就會(huì)涉及到JVM同步鎖。用JVM同步鎖來(lái)解決開(kāi)發(fā)中的這類(lèi)為題也完全可以早处,一個(gè)是空間換時(shí)間湾蔓,一個(gè)是時(shí)間換空間。

到這里這個(gè)ThreadLocal就講完了砌梆,歡迎交流卵蛉。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市么库,隨后出現(xiàn)的幾起案子傻丝,更是在濱河造成了極大的恐慌,老刑警劉巖诉儒,帶你破解...
    沈念sama閱讀 219,427評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件葡缰,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡忱反,警方通過(guò)查閱死者的電腦和手機(jī)泛释,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)温算,“玉大人怜校,你說(shuō)我怎么就攤上這事屁桑⊙椋” “怎么了极景?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,747評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵森枪,是天一觀的道長(zhǎng)惠拭。 經(jīng)常有香客問(wèn)我莫杈,道長(zhǎng)蛙婴,這世上最難降的妖魔是什么敷钾? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,939評(píng)論 1 295
  • 正文 為了忘掉前任宣谈,我火速辦了婚禮愈犹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘闻丑。我一直安慰自己漩怎,他們只是感情好勋颖,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著勋锤,像睡著了一般饭玲。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上怪得,一...
    開(kāi)封第一講書(shū)人閱讀 51,737評(píng)論 1 305
  • 那天咱枉,我揣著相機(jī)與錄音,去河邊找鬼徒恋。 笑死蚕断,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的入挣。 我是一名探鬼主播亿乳,決...
    沈念sama閱讀 40,448評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼径筏!你這毒婦竟也來(lái)了葛假?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,352評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤滋恬,失蹤者是張志新(化名)和其女友劉穎聊训,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體恢氯,經(jīng)...
    沈念sama閱讀 45,834評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡带斑,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了勋拟。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片勋磕。...
    茶點(diǎn)故事閱讀 40,133評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖敢靡,靈堂內(nèi)的尸體忽然破棺而出挂滓,到底是詐尸還是另有隱情,我是刑警寧澤啸胧,帶...
    沈念sama閱讀 35,815評(píng)論 5 346
  • 正文 年R本政府宣布赶站,位于F島的核電站,受9級(jí)特大地震影響吓揪,放射性物質(zhì)發(fā)生泄漏亲怠。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評(píng)論 3 331
  • 文/蒙蒙 一柠辞、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧主胧,春花似錦叭首、人聲如沸习勤。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,022評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)图毕。三九已至,卻和暖如春眷唉,著一層夾襖步出監(jiān)牢的瞬間予颤,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,147評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工冬阳, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蛤虐,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,398評(píng)論 3 373
  • 正文 我出身青樓肝陪,卻偏偏與公主長(zhǎng)得像驳庭,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子氯窍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評(píng)論 2 355