一:ThreadLocal是什么扁凛?
學(xué)習(xí)JDK的類最好的辦法就是先看一下源碼上的注解
從JAVA官方對ThreadLocal類的說明定義(定義在示例代碼中):ThreadLocal類用來提供線程內(nèi)部的局部變量沽瘦。這種變量在多線程環(huán)境下訪問(通過get和set方法訪問)時(shí)能保證各個(gè)線程的變量相對獨(dú)立于其他線程內(nèi)的變量袍祖。ThreadLocal實(shí)例通常來說都是private static類型的肉康,用于關(guān)聯(lián)線程和線程上下文物咳。
我們可以得知ThreadLocal的作用是:ThreadLocal的作用是提供線程內(nèi)的局部變量憔四,不同的線程之間不會(huì)相互干擾伐憾,這種變量在線程的生命周期內(nèi)起作用,減少同一個(gè)線程內(nèi)多個(gè)函數(shù)或組件之間一些公共變量的傳遞的復(fù)雜度秒际。
上述可以概述為:ThreadLocal提供線程內(nèi)部的局部變量悬赏,在本線程內(nèi)隨時(shí)隨地可取,隔離其他線程娄徊。
二:ThreadLocal如何使用闽颇?
學(xué)習(xí)一個(gè)JDK工具類,首先是使用它寄锐,再回頭了解源碼和設(shè)計(jì)理念兵多,所以我們先看它是如何去運(yùn)用的。
首先我定義一下這次運(yùn)用所使用的數(shù)據(jù)載體
可以看到上圖橄仆,我定義了一個(gè)類剩膘,這個(gè)類就是描述語言環(huán)境的一個(gè)結(jié)構(gòu)體。
在很多RPC或者webservice交互中盆顾,或多或少有國際化的需求怠褐,那么為了減少重復(fù)設(shè)置語言環(huán)境以及方法入?yún)⒃黾诱Z言參數(shù),將國際化語言環(huán)境放入ThreadLocal中是一個(gè)不錯(cuò)的選擇椎扬,它也是代表一個(gè)上下文Transaction的限界惫搏,所以本文的樣例選用這個(gè)類作為ThreadLocalMap的載體具温,另外提供Builder模式進(jìn)行create,覆寫toString進(jìn)行測試蚕涤。
下面看一下ThreadLocal是如何去運(yùn)用的
如上圖,ThreadLocal可以以這種方式去運(yùn)用铣猩,首先定義一個(gè)工具類的概念揖铜,將Context的概念封裝在工具類里。
而存放Context載體的介質(zhì)就是ThreadLocal达皿,他提供initialValue天吓、set、get峦椰、remove等方法龄寞,而工具類中靜態(tài)方法對這些方法做了一層適配,其實(shí)有點(diǎn)類似于代理或者適配汤功,不過這里的目的是使setLanguageContext等方法更明確含義物邑,封裝了底層細(xì)節(jié),對上層模塊友好。
當(dāng)然色解,這里的例子泛型也可以換為String或者其他類型的茂嗓,并且也可以省略初始化方法的覆寫,一切取決于需求科阎,這里只是提供方法并不糾結(jié)具象述吸。
Ok,我們來試一下這個(gè)類的使用效果锣笨。
可以看到蝌矛,他能夠隔離開線程,使用自己的私有變量错英,第一次循環(huán)和第二次循環(huán)打印出的language字段并不同朴读。
三:ThreadLocal源碼與設(shè)計(jì)思想
上面已經(jīng)介紹了ThreadLocal的作用和運(yùn)用方式,那么在使用完它以后走趋,我們來探尋一下它實(shí)現(xiàn)的原理衅金,和它設(shè)計(jì)的思想是什么,學(xué)習(xí)原理簿煌,知其然知其所以然氮唯,有助于我們避開ThreadLocal的使用誤區(qū),也有助于理解如何選擇什么樣的場景適合ThreadLocal姨伟。
看一下源碼
先看一下它的靜態(tài)內(nèi)部類惩琉,ThreadLocalMap
ThreadLocalMap維護(hù)了Entry環(huán)形數(shù)組,數(shù)組中元素Entry的邏輯上的key為某個(gè)ThreadLocal對象(實(shí)際上是指向該ThreadLocal對象的弱引用)夺荒,value為代碼中該線程往該ThreadLoacl變量實(shí)際塞入的值瞒渠。
讀到這里,如果不問不答為什么是這樣的定義形式技扼,為什么要用弱引用伍玖,等于沒讀懂源碼。
因?yàn)槿绻@里使用普通的key-value形式來定義存儲(chǔ)結(jié)構(gòu)剿吻,實(shí)質(zhì)上就會(huì)造成節(jié)點(diǎn)的生命周期與線程強(qiáng)綁定窍箍,只要線程沒有銷毀,那么節(jié)點(diǎn)在GC分析中一直處于可達(dá)狀態(tài)丽旅,沒辦法被回收椰棘,而程序本身也無法判斷是否可以清理節(jié)點(diǎn)。弱引用是Java中四檔引用的第三檔榄笙,比軟引用更加弱一些邪狞,如果一個(gè)對象沒有強(qiáng)引用鏈可達(dá),那么一般活不過下一次GC茅撞。當(dāng)某個(gè)ThreadLocal已經(jīng)沒有強(qiáng)引用可達(dá)帆卓,則隨著它被垃圾回收杆逗,在ThreadLocalMap里對應(yīng)的Entry的鍵值會(huì)失效,這為ThreadLocalMap本身的垃圾清理提供了便利鳞疲。
上面的這些屬性以及方法罪郊,是Entry的定義。
可以看到rehash的負(fù)載因子是設(shè)置最壞三分之二容量擴(kuò)容尚洽。
而容量默認(rèn)是16悔橄,注解Must be a power of two,為什么一定要是2的次冪腺毫,我理解的原因有兩個(gè)癣疟,第一是ThreadLocalMap使用的是線性探測法,均勻分布的好處在于很快就能探測到下一個(gè)臨近的可用slot潮酒,從而保證效率睛挚。第二是位運(yùn)算比取模效率高,rehash的時(shí)候只需要判斷0還是1急黎。
來看一看關(guān)于ThreadLocal的set方法實(shí)現(xiàn)
這個(gè)代碼再簡單不過了扎狱,不多介紹了,getMap方法的實(shí)現(xiàn)是傳入一個(gè)Thread引用勃教,返回一個(gè)ThreadLocalMap淤击,見下圖
原來Thread類將ThreadLocalMap作為了一個(gè)全局變量,getMap方法只是拿到這個(gè)變量故源,如果不是空污抬,就放值進(jìn)entry,如果是空绳军,就創(chuàng)建一個(gè)ThreadLocalMap指向當(dāng)前線程threadLocals變量印机。
再看看get方法
還是一樣的流程,先拿到當(dāng)前線程门驾,然后拿到當(dāng)前線程內(nèi)的ThreadLocalMap
重點(diǎn)是getEntry方法射赛。
上圖的table是Object[],其實(shí)就是個(gè)map猎唁,這里的map用的是線性探測解決沖突咒劲,而hashmap是用拉鏈法顷蟆。
調(diào)用getEntryAfterMiss線性探測诫隅,過程中每碰到無效slot,調(diào)用expungeStaleEntry進(jìn)行段清理帐偎;如果找到了key逐纬,則返回結(jié)果entry,所以這里get也有去除一些無效引用的作用削樊,上面的while語句就是個(gè)線性探測豁生。
看一下remove
remove方法相對于getEntry和set方法比較簡單兔毒,直接在table中找key,如果找到了甸箱,把弱引用斷了做一次段清理育叁。
至此源碼部分就簡單介紹完了,關(guān)于Entry內(nèi)部實(shí)現(xiàn)還有很多沒有詳細(xì)介紹的芍殖,因?yàn)槲矣X得關(guān)于散列還有性能方面設(shè)計(jì)豪嗽,其實(shí)并不是我這次分享主要的目的,主要的目的是介紹另一種線程安全的實(shí)現(xiàn)手段豌骏,是一個(gè)宏觀的概念龟梦,如果讀者自己對于ThreadLocal在性能上,內(nèi)存泄漏上窃躲,斐波那契散列等內(nèi)容感興趣计贰,可以自己去仔細(xì)閱讀源碼。
最后總結(jié)一下ThreadLocal的設(shè)計(jì)理念蒂窒。
首先堆內(nèi)存是共享的躁倒,那么必須共享堆的時(shí)候,有加鎖和不變性等方式去解決數(shù)據(jù)共享問題洒琢。
還有還有以空間換時(shí)間的思路樱溉,ThreadLocal就屬于后者。
項(xiàng)目代碼:https://github.com/Spring5945/Concurrent