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)是如下圖這樣的:
每個(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ǔ)方案萝毛?
先看set方法:
先拿到當(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í)例通今,是被弱引用的。
目的就是在threadlocal被回收的時(shí)候肛根,能清除掉數(shù)組里的過(guò)期槽位(所謂過(guò)期槽位就是key為null的槽)辫塌。
這個(gè)ThreadLocalMap是線程Thread持有的一個(gè)成員變量。
由此對(duì)應(yīng)到上面那張我手畫(huà)的圖派哲,每個(gè)Thread持有一個(gè)ThreadLocalMap臼氨。
看下map的創(chuàng)建:
它只有一個(gè)構(gòu)造函數(shù),且沒(méi)有提供設(shè)置初始化數(shù)組大小的入口狮辽,所以我說(shuō)這個(gè)16的初始值沒(méi)法手動(dòng)修改一也。但是如果set的數(shù)據(jù)超過(guò)裝載因子巢寡,就會(huì)進(jìn)行rehash。
這個(gè)threshold的值是size的三分之二:
然后我們?cè)倏聪聄ehash:
如上圖椰苟,超過(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就講完了砌梆,歡迎交流卵蛉。