ThreadLocal是一個(gè)線程內(nèi)部的數(shù)據(jù)存儲(chǔ)類,它用來存儲(chǔ)那種---以線程為作用域并且不同線程具有不同的數(shù)據(jù)副本的這類數(shù)據(jù)。
如果沒有這個(gè)東西,如果我們要實(shí)現(xiàn)線程隔離的一些數(shù)據(jù)副本的存儲(chǔ),該怎么做?我們會(huì)創(chuàng)建一個(gè)當(dāng)前進(jìn)程下的,全局哈希表。這個(gè)哈希表對(duì)所有線程可見。但是這樣做會(huì)有三個(gè)問題:
- 需要為每個(gè)存儲(chǔ)的對(duì)象都創(chuàng)建一個(gè)哈希表,比如面向Looper的哈希表和面向某個(gè)String對(duì)象的哈希表。亦或是只用一個(gè)哈希表,但是哈希表里的桶,需要預(yù)估存儲(chǔ)的數(shù)據(jù)量和數(shù)據(jù)類型,然后采用相應(yīng)的存儲(chǔ)結(jié)構(gòu)。
- 既然是當(dāng)前虛擬機(jī)內(nèi)所有的線程可見,那就需要處理并發(fā)讀寫的問題,涉及到加鎖,容易出錯(cuò)。
于是相比較之下,還是ThreadLocal的方案更優(yōu)雅。
在這個(gè)基礎(chǔ)上還可以解決復(fù)雜邏輯下的對(duì)象傳遞,比如傳遞監(jiān)聽器。
否則只能直接通過參數(shù)的形式傳遞監(jiān)聽器或者把監(jiān)聽器定義成靜態(tài)變量。前者在調(diào)用棧很深的時(shí)候無法接受,后者不具備可擴(kuò)展性,可能會(huì)有很多靜態(tài)監(jiān)聽器對(duì)象。
它的大致結(jié)構(gòu)是如下圖這樣的:
每個(gè)線程Thread會(huì)持有一個(gè)ThreadLocalMap對(duì)象,這個(gè)對(duì)象是一個(gè)長(zhǎng)度為16的數(shù)組,數(shù)組里存放我們剛剛說的數(shù)據(jù)副本。這個(gè)數(shù)組的桶里是一個(gè)K,V對(duì),key是我們創(chuàng)建的ThreadLocal對(duì)象本身,value就是真正存儲(chǔ)的數(shù)據(jù)。也就是說每個(gè)線程能存放的數(shù)據(jù)量是16個(gè)對(duì)象。能不能擴(kuò)展呢,不可以手動(dòng)擴(kuò)展,至少在android-28的源碼里,是沒有擴(kuò)展的入口的。但是在數(shù)據(jù)插入超過裝載因子的情況下,會(huì)進(jìn)行擴(kuò)容。
至此這個(gè)TL的原理就講完了,接下來會(huì)涉及到android平臺(tái)相關(guān)的一些代碼細(xì)節(jié)來證實(shí),不是必看內(nèi)容。
它是如何通過這樣簡(jiǎn)單的get和set,完成這種線程間相互隔離的數(shù)據(jù)存儲(chǔ)方案?
先看set方法:
先拿到當(dāng)前的線程t----然后根據(jù)當(dāng)前線程t來得到當(dāng)前線程的ThreadLocalMap,如果沒有就創(chuàng)建。有的話,就調(diào)用set方法,這個(gè)this就是我們創(chuàng)建的threadlocal實(shí)例,value就是具體的數(shù)據(jù)。
這里值得一提的是,這個(gè)K,V對(duì),里的key也就是ThreadLocal的實(shí)例,是被弱引用的。
目的就是在threadlocal被回收的時(shí)候,能清除掉數(shù)組里的過期槽位(所謂過期槽位就是key為null的槽)。
這個(gè)ThreadLocalMap是線程Thread持有的一個(gè)成員變量。
由此對(duì)應(yīng)到上面那張我手畫的圖,每個(gè)Thread持有一個(gè)ThreadLocalMap。
看下map的創(chuàng)建:
它只有一個(gè)構(gòu)造函數(shù),且沒有提供設(shè)置初始化數(shù)組大小的入口,所以我說這個(gè)16的初始值沒法手動(dòng)修改。但是如果set的數(shù)據(jù)超過裝載因子,就會(huì)進(jìn)行rehash。
這個(gè)threshold的值是size的三分之二:
然后我們?cè)倏聪聄ehash:
如上圖,超過裝載因子以后會(huì)擴(kuò)容成原來的2倍大。即新建一個(gè)兩倍大的數(shù)組,然后把原始拷貝過去,這個(gè)過程和arraylist的擴(kuò)容操作類似,其實(shí)數(shù)組這中結(jié)構(gòu),擴(kuò)容的辦法都是這樣的,先復(fù)制,再拷貝。
回到剛剛的set方法,補(bǔ)充一句,是先對(duì)key進(jìn)行hash,之后計(jì)算出理論的槽位,然后嘗試放入,槽位為空或者key為null直接覆蓋,否則就嘗試下一個(gè)index(即 用線性探測(cè)法解決哈希沖突)。
在ThreadLocal的使用過程中,可能出現(xiàn)內(nèi)存泄漏和線程不安全的情況。
-
內(nèi)存泄漏
前面說過了,kv對(duì)中的key是用弱引用持有的ThreadLocal的實(shí)例,當(dāng)key被回收以后,value會(huì)在下次set的時(shí)候被當(dāng)做過期的槽位清空。
但是這個(gè)不夠及時(shí),如果沒有下個(gè)set操作的到來,線程也遲遲不結(jié)束,就會(huì)存在對(duì)value的強(qiáng)引用因?yàn)関alue不會(huì)被訪問了但是釋放不掉導(dǎo)致內(nèi)存泄漏。只能等當(dāng)前thread結(jié)束以后,強(qiáng)引用被斷開,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è)線程不安全,翻譯過來就是,能在A線程里的ThreadLocal更改B線程里的ThreadLocal。
應(yīng)該避免這種情況,即不同線程里的ThreadLocal持有同一個(gè)對(duì)象(靜態(tài)對(duì)象)(static修飾的類在JVM中只保存一個(gè)實(shí)例對(duì)象)。
總結(jié)提煉一下,ThreadLocal的意義是什么?回顧文章開頭我對(duì)比哪個(gè)全局哈希表的解決方案。其實(shí)ThreadLocal是解決線程安全的一個(gè)好辦法,為每個(gè)線程提供了獨(dú)立的變量副本解決了線程共享變量并發(fā)訪問的問題。這個(gè)并發(fā)訪問就會(huì)涉及到JVM同步鎖。用JVM同步鎖來解決開發(fā)中的這類為題也完全可以,一個(gè)是空間換時(shí)間,一個(gè)是時(shí)間換空間。
到這里這個(gè)ThreadLocal就講完了,歡迎交流。