ThreadLocal是java.lang包里的一個優(yōu)秀的多線程工具。ThreadLocal為變量在每個線程中都創(chuàng)建了一個副本,每個線程可以訪問自己內(nèi)部的副本變量,保證線程的安全。另一方面ThreadLocal也可以作為閱讀源碼的起點(diǎn),代碼量不多,但是設(shè)計的十分巧妙。
ThreadLocal原理
每個線程Thread持有一個變量ThreadLocalMap,Map的key就是ThreadLocal變量,value則是我們要存儲的值。
使用threadLocal.set()方法儲值時,首先通過Thread.currentThread()方法得到當(dāng)前的線程,然后拿到當(dāng)前線程持有的ThreadLocalMap,將鍵值對存在ThreadLocalMap中。
使用threadLocal.get()方法取值時,首先通過Thread.currentThread()方法得到當(dāng)前的線程,然后拿到當(dāng)前線程持有的ThreadLocalMap,根據(jù)threadLocal作為key查找對應(yīng)的value,如果沒取到會set一個value為默認(rèn)值的鍵值對,并將默認(rèn)值返回。
變量值實際上存儲在線程Thread中的ThreadLocalMap中,因為每個線程都有屬于自己的ThreadLocalMap,每個線程也只能操作屬于自己的ThreadLocalMap,這就既保障了變量的線程安全,又為一些特殊的場景提供了一個有力的工具。
ThreadLocalMap原理
上面說到鍵值對都存在了ThreadLocalMap中,這是一個自己實現(xiàn)的類似于HashMap的集合。ThreadLocalMap使用內(nèi)部靜態(tài)類Entry來儲值,Entry包含了對ThreadLocal的弱引用以及對值的強(qiáng)引用。這里為什么對Key使用弱引用,在下面會講。ThreadLocalMap用哈希算法維護(hù)一個Entry數(shù)組用以存儲Entry,用開放地址法來解決哈希沖突。
每個ThreadLocal對象都有一個屬于自己的threadLocalHashCode,這個值由靜態(tài)AtomicInteger變量nextHashCode每次加0x61c88647來維護(hù),保證每個ThreadLocal的threadLocalHashCode值不同,并且最大程度下不會產(chǎn)生哈希碰撞。在get、set方法中,ThreadLocalMap使用threadLocalHashCode與數(shù)組的大小減一進(jìn)行與運(yùn)算,求得ThreadLocal在數(shù)組中的位置,如果此時產(chǎn)生了哈希沖突則遍歷數(shù)組后面的位置一邊清理ThreadLocalMap中key為null的鍵值對,一邊找到空位儲值或從位置中取值。
這里引申一點(diǎn),這種線性探測的開放地址法在性能上有些差,當(dāng)然這么設(shè)計有一方面是考慮要順便清理廢棄的鍵值對。因此Netty自己實現(xiàn)了一個FastThreadLocal來提升性能,有機(jī)會在以后會介紹一下FastThreadLocal的實現(xiàn)。
ThreadLocal線程泄露問題
上面說到了ThreadLocalMap是線程Thread持有的一個變量,而ThreadLocalMap中存放著作為key的ThreadLocal對象和作為value的我們存放的變量。需要注意的是在其他引用都是強(qiáng)引用的情況下,ThreadLocalMap對ThreadLocal的引用則是弱引用。這使得ThreadLocal在失去其他強(qiáng)引用時會被jvm回收掉,但是鍵值對中的value還保持著ThreadLocalMap本身的強(qiáng)引用不會被回收。這些value會隨著ThreadLocalMap一直活到Thread的生命周期結(jié)束,也就是會造成一定意義上的內(nèi)存泄露問題。
為了一定程度上的解決這個問題,ThreadLocalMap提供的set、get、remove方法都會清理ThreadLocalMap中key為null的鍵值對。但是如果線程一直存活且不調(diào)用這些方法還是會產(chǎn)生泄露問題,因此使用完變量后最好將其remove掉。
如果ThreadLocalMap使用強(qiáng)引用引用ThreadLocal會造成更嚴(yán)重的內(nèi)存泄露問題。ThreadLocal在失去其他強(qiáng)引用時因為ThreadLocalMap對它一直保持著強(qiáng)引用而無法被回收掉。ThreadLocalMap也無法簡單地判斷哪個鍵值對是需要被回收的,只好引用著所有鍵值對直到線程生命周期結(jié)束后一起被回收。
這里引申一下Tomcat。Tomcat的Server組件中有一個ThreadLocalLeakPreventionListener監(jiān)聽器專門用來處理ThreadLocal可能造成的內(nèi)存泄露問題。Tomcat的context重載時采用的方法是重新初始化一個Webclassloader類加載器。當(dāng)context重載的時候,如果正在執(zhí)行的線程引用了threadlocal中的對象,而該對象由Webclassloader加載,會造成整個webclassloader回收不了,從而造成內(nèi)存泄露。Tomcat給出的解決方法是重載時把線程池中的所有線程銷毀且重新創(chuàng)建,這樣就不會有泄露的問題了。
InheritableThreadLocal
InheritableThreadLocal是ThreadLocal的子類,繼承了ThreadLocal的所有使用方法。Thread中其實持有了兩個ThreadLocalMap,一個比較常見用來存放ThreadLocal,另一個則用來存放InheritableThreadLocal。InheritableThreadLocal的特殊之處在于父線程新建線程時會將所有ThreadLocalMap中的InheritableThreadLocal繼承給子線程。子線程在初始化時就會將父線程的所有InheritableThreadLocal鍵值對復(fù)制到子線程中,需要注意的是父子線程鍵值對的value引用的是同一個對象(相當(dāng)于淺度拷貝),使用時需要考慮線程安全問題。