ThreadLocal是java.lang包里的一個(gè)優(yōu)秀的多線程工具剧防。ThreadLocal為變量在每個(gè)線程中都創(chuàng)建了一個(gè)副本,每個(gè)線程可以訪問自己內(nèi)部的副本變量,保證線程的安全狠持。另一方面ThreadLocal也可以作為閱讀源碼的起點(diǎn),代碼量不多瞻润,但是設(shè)計(jì)的十分巧妙喘垂。
ThreadLocal原理
每個(gè)線程Thread持有一個(gè)變量ThreadLocalMap,Map的key就是ThreadLocal變量绍撞,value則是我們要存儲(chǔ)的值正勒。
使用threadLocal.set()方法儲(chǔ)值時(shí),首先通過Thread.currentThread()方法得到當(dāng)前的線程傻铣,然后拿到當(dāng)前線程持有的ThreadLocalMap章贞,將鍵值對(duì)存在ThreadLocalMap中。
使用threadLocal.get()方法取值時(shí)非洲,首先通過Thread.currentThread()方法得到當(dāng)前的線程鸭限,然后拿到當(dāng)前線程持有的ThreadLocalMap蜕径,根據(jù)threadLocal作為key查找對(duì)應(yīng)的value,如果沒取到會(huì)set一個(gè)value為默認(rèn)值的鍵值對(duì)败京,并將默認(rèn)值返回兜喻。
變量值實(shí)際上存儲(chǔ)在線程Thread中的ThreadLocalMap中,因?yàn)槊總€(gè)線程都有屬于自己的ThreadLocalMap喧枷,每個(gè)線程也只能操作屬于自己的ThreadLocalMap虹统,這就既保障了變量的線程安全,又為一些特殊的場(chǎng)景提供了一個(gè)有力的工具隧甚。
ThreadLocalMap原理
上面說到鍵值對(duì)都存在了ThreadLocalMap中车荔,這是一個(gè)自己實(shí)現(xiàn)的類似于HashMap的集合。ThreadLocalMap使用內(nèi)部靜態(tài)類Entry來儲(chǔ)值戚扳,Entry包含了對(duì)ThreadLocal的弱引用以及對(duì)值的強(qiáng)引用忧便。這里為什么對(duì)Key使用弱引用,在下面會(huì)講帽借。ThreadLocalMap用哈希算法維護(hù)一個(gè)Entry數(shù)組用以存儲(chǔ)Entry珠增,用開放地址法來解決哈希沖突。
每個(gè)ThreadLocal對(duì)象都有一個(gè)屬于自己的threadLocalHashCode砍艾,這個(gè)值由靜態(tài)AtomicInteger變量nextHashCode每次加0x61c88647來維護(hù)蒂教,保證每個(gè)ThreadLocal的threadLocalHashCode值不同,并且最大程度下不會(huì)產(chǎn)生哈希碰撞脆荷。在get凝垛、set方法中,ThreadLocalMap使用threadLocalHashCode與數(shù)組的大小減一進(jìn)行與運(yùn)算蜓谋,求得ThreadLocal在數(shù)組中的位置梦皮,如果此時(shí)產(chǎn)生了哈希沖突則遍歷數(shù)組后面的位置一邊清理ThreadLocalMap中key為null的鍵值對(duì),一邊找到空位儲(chǔ)值或從位置中取值桃焕。
這里引申一點(diǎn)剑肯,這種線性探測(cè)的開放地址法在性能上有些差,當(dāng)然這么設(shè)計(jì)有一方面是考慮要順便清理廢棄的鍵值對(duì)观堂。因此Netty自己實(shí)現(xiàn)了一個(gè)FastThreadLocal來提升性能让网,有機(jī)會(huì)在以后會(huì)介紹一下FastThreadLocal的實(shí)現(xiàn)。
ThreadLocal線程泄露問題
上面說到了ThreadLocalMap是線程Thread持有的一個(gè)變量师痕,而ThreadLocalMap中存放著作為key的ThreadLocal對(duì)象和作為value的我們存放的變量寂祥。需要注意的是在其他引用都是強(qiáng)引用的情況下,ThreadLocalMap對(duì)ThreadLocal的引用則是弱引用七兜。這使得ThreadLocal在失去其他強(qiáng)引用時(shí)會(huì)被jvm回收掉,但是鍵值對(duì)中的value還保持著ThreadLocalMap本身的強(qiáng)引用不會(huì)被回收福扬。這些value會(huì)隨著ThreadLocalMap一直活到Thread的生命周期結(jié)束腕铸,也就是會(huì)造成一定意義上的內(nèi)存泄露問題惜犀。
為了一定程度上的解決這個(gè)問題,ThreadLocalMap提供的set狠裹、get虽界、remove方法都會(huì)清理ThreadLocalMap中key為null的鍵值對(duì)。但是如果線程一直存活且不調(diào)用這些方法還是會(huì)產(chǎn)生泄露問題涛菠,因此使用完變量后最好將其remove掉莉御。
如果ThreadLocalMap使用強(qiáng)引用引用ThreadLocal會(huì)造成更嚴(yán)重的內(nèi)存泄露問題。ThreadLocal在失去其他強(qiáng)引用時(shí)因?yàn)門hreadLocalMap對(duì)它一直保持著強(qiáng)引用而無法被回收掉俗冻。ThreadLocalMap也無法簡(jiǎn)單地判斷哪個(gè)鍵值對(duì)是需要被回收的礁叔,只好引用著所有鍵值對(duì)直到線程生命周期結(jié)束后一起被回收。
這里引申一下Tomcat迄薄。Tomcat的Server組件中有一個(gè)ThreadLocalLeakPreventionListener監(jiān)聽器專門用來處理ThreadLocal可能造成的內(nèi)存泄露問題琅关。Tomcat的context重載時(shí)采用的方法是重新初始化一個(gè)Webclassloader類加載器。當(dāng)context重載的時(shí)候讥蔽,如果正在執(zhí)行的線程引用了threadlocal中的對(duì)象涣易,而該對(duì)象由Webclassloader加載,會(huì)造成整個(gè)webclassloader回收不了冶伞,從而造成內(nèi)存泄露新症。Tomcat給出的解決方法是重載時(shí)把線程池中的所有線程銷毀且重新創(chuàng)建,這樣就不會(huì)有泄露的問題了响禽。
InheritableThreadLocal
InheritableThreadLocal是ThreadLocal的子類徒爹,繼承了ThreadLocal的所有使用方法。Thread中其實(shí)持有了兩個(gè)ThreadLocalMap金抡,一個(gè)比較常見用來存放ThreadLocal瀑焦,另一個(gè)則用來存放InheritableThreadLocal。InheritableThreadLocal的特殊之處在于父線程新建線程時(shí)會(huì)將所有ThreadLocalMap中的InheritableThreadLocal繼承給子線程梗肝。子線程在初始化時(shí)就會(huì)將父線程的所有InheritableThreadLocal鍵值對(duì)復(fù)制到子線程中榛瓮,需要注意的是父子線程鍵值對(duì)的value引用的是同一個(gè)對(duì)象(相當(dāng)于淺度拷貝),使用時(shí)需要考慮線程安全問題巫击。