這篇文章寫的好了http://duanqz.github.io/2018-03-15-Java-ThreadLocal
同時這篇文章關(guān)于內(nèi)存泄漏講的挺好的http://www.importnew.com/22039.html
http://www.iteye.com/topic/103804
ThreadLocal,在沒有任何背景知識的情況下,我們從英文單詞的意思上理解它:
Thread:跟線程相關(guān)。Java語言中,表示線程的類就是Thread鹅颊,是程序最小的執(zhí)行單元,多個線程可以并發(fā)執(zhí)行。
Local:本地财松、局部纱控,與之相對的概念就是遠(yuǎn)程舶掖、全局眨攘。Java語言中,通常用Local表示局部變量龟虎。
這兩個概念一組合鲤妥,拼成了英文單詞ThreadLocal棉安,線程局部铸抑?局部線程蒲赂?究竟要表達(dá)什么意思木蹬,為什么不叫LocalThread镊叁,完全找不著北啊蛔添!
筆者搜羅了網(wǎng)上對ThreadLocal的一些解讀:
ThreadLocal為解決多線程程序的并發(fā)問題提供了一種新的思路夸溶。使用這個工具類可以很簡潔地編寫出優(yōu)美的多線程程序扫皱,ThreadLocal并不是一個Thread,而是Thread的局部變量段多,把它命名為ThreadLocalVariable更容易讓人理解一些
ThreadLocal并不是用來并發(fā)控制訪問一個共同對象进苍,而是為了給每個線程分配一個只屬于該線程的變量。它的功用非常簡單鸭叙,就是為每一個使用該變量的線程都提供一個變量值的副本觉啊,是每一個線程都可以獨(dú)立地改變自己的副本,而不會和其它線程的副本沖突沈贝,實(shí)現(xiàn)線程間的數(shù)據(jù)隔離杠人。從線程的角度看,就好像每一個線程都完全擁有該變量
ThreadLocal類用來提供線程內(nèi)部的局部變量。這種變量在多線程環(huán)境下訪問(通過get或set方法訪問)時能保證各個線程里的變量相對獨(dú)立于其他線程內(nèi)的變量嗡善。ThreadLocal實(shí)例通常來說都是private static類型的市俊,用于關(guān)聯(lián)線程和線程的上下文
引入ThreadLocal的初衷是為了提供線程內(nèi)的局部變量,而不是為了解決共享對象的多線程訪問問題滤奈。實(shí)際上摆昧,ThreadLocal根本就不能解決共享對象的多線程訪問問題
1. ThreadLocal 實(shí)現(xiàn)原理
ThreadLocal的實(shí)現(xiàn)是這樣的:每個Thread維護(hù)一個ThreadLocalMap映射表,這個映射表的key是ThreadLocal實(shí)例本身蜒程,value是真正需要存儲的Object绅你。
也就是說ThreadLocal本身并不存儲值,它只是作為一個key來讓線程從ThreadLocalMap獲取value昭躺。值得注意的是圖中的虛線忌锯,表示ThreadLocalMap是使用ThreadLocal的弱引用作為Key的,弱引用的對象在 GC 時會被回收领炫。
線程類Thread中有一個類型為ThreadLocalMap的變量為threadLocals
ThreadLocalMap是一個映射表偶垮,內(nèi)部實(shí)現(xiàn)是一個數(shù)組,每一個元素的類型為Entry
Entry就是一個鍵值對(Key-Value Pair)帝洪,其?Key?就是ThreadLocal似舵,其?Value?可以是任何對象
1.1 ThreadLocal的主要接口
set(),表示要往當(dāng)前線程中設(shè)置“本地變量”葱峡,最終的結(jié)果是將變量設(shè)置到了線程的映射表砚哗。
get(),表示要從當(dāng)前線程中取出“本地變量”砰奕,最終的結(jié)果是在當(dāng)前線程的映射表中蛛芥,以調(diào)用get()方法的ThreadLocal對象為Key,查詢出對應(yīng)的Value军援。
ThreadLocal的set()和get()方法的主體邏輯算是比較簡單了仅淑,圍繞主體邏輯,還做了一些特殊處理胸哥,譬如:線程中的映射表還未初始化時涯竟,調(diào)用createMap()進(jìn)行初始化;在映射表中沒有獲取到Value時烘嘱,通過setInitialValue()設(shè)置一個初始值昆禽,這種場景下,只需要實(shí)現(xiàn)initialValue()函數(shù)就可以了蝇庭,這種ThreadLocal的使用方式很常見醉鳖。本文不再展開這些細(xì)枝末節(jié)的邏輯,讀者自行閱讀源碼即可哮内。
1.2 ThreadLocalMap映射表
ThreadLocal并不是一個存儲容器盗棵,往ThreadLocal中讀(get)和寫(set)數(shù)據(jù)壮韭,其實(shí)都是將數(shù)據(jù)保存到了每個線程自己的存儲空間。
線程中的存儲空間是一個映射表(ThreadLocalMap)纹因,TheadLocal其實(shí)就是這個映射表每一項(xiàng)的Key喷屋,通過ThreadLocal讀寫數(shù)據(jù),其實(shí)就是通過Key在一個映射表中讀寫數(shù)據(jù)瞭恰。
上文中圖示中屯曹,我們見過映射表的結(jié)構(gòu),它是一個名為table的數(shù)組惊畏,每一個元素都是Entry對象恶耽,而Entry對象包含key和value兩個屬性,其代碼如下所示:
ThreadLocalMap的Entry是WeakReference的子類颜启,這樣能保證線程中的映射表的每一個Entry可以被垃圾回收偷俭,而不至于發(fā)生內(nèi)存泄露。因?yàn)?b>ThreadLocal作為全局的Key缰盏,其生命周期很可能比一個線程要長涌萤,如果Entry是一個強(qiáng)引用,那么線程對象就一直持有ThreadLocal的引用口猜,而不能被釋放负溪。隨著線程越來越多,這些不能被釋放的內(nèi)存也就越來越多暮的。
ThreadLocal作為映射表的Key笙以,需要具備唯一的標(biāo)識,每創(chuàng)建一個新的ThreadLocal冻辩,這個標(biāo)識就變的跟之前不一樣了。 如何保證每一個ThreadLocal的唯一性呢拆祈?
ThreadLocal內(nèi)部有一個名為threadLocalHashCode的變量恨闪,每創(chuàng)建一個新的ThreadLocal對象,這個變量的值就會增加0x61c88647放坏。 正是因?yàn)橛羞@么一個神奇的數(shù)字咙咽,它能夠保證生成的Hash值可以均勻的分布在0~(2^N-1)之間,N是數(shù)組長度淤年。 更多關(guān)于數(shù)字0x61c88647钧敞,可以參考Why 0x61c88647?
2. 使用場景
下面來看一個hibernate中典型的ThreadLocal的應(yīng)用:?
Java代碼?
private?static?final?ThreadLocal?threadSession?=?new?ThreadLocal();??
public?static?Session?getSession()?throws?InfrastructureException?{??
????Session?s?=?(Session)?threadSession.get();??
try?{??
if?(s?==?null)?{??
????????????s?=?getSessionFactory().openSession();??
????????????threadSession.set(s);??
????????}??
}catch?(HibernateException?ex)?{??
throw?new?InfrastructureException(ex);??
????}??
return?s;??
}??
可以看到在getSession()方法中,首先判斷當(dāng)前線程中有沒有放進(jìn)去session麸粮,如果還沒有溉苛,那么通過sessionFactory().openSession()來創(chuàng)建一個session,再將session set到線程中弄诲,實(shí)際是放到當(dāng)前線程的ThreadLocalMap這個map中愚战,這時,對于這個session的唯一引用就是當(dāng)前線程中的那個ThreadLocalMap(下面會講到),而threadSession作為這個值的key寂玲,要取得這個session可以通過threadSession.get()來得到塔插,里面執(zhí)行的操作實(shí)際是先取得當(dāng)前線程中的ThreadLocalMap,然后將threadSession作為key將對應(yīng)的值取出拓哟。這個session相當(dāng)于線程的私有變量想许,而不是public的。 顯然断序,其他線程中是取不到這個session的伸刃,他們也只能取到自己的ThreadLocalMap中的東西
3.ThreadLocal容易導(dǎo)致內(nèi)存泄漏的問題
ThreadLocalMap使用ThreadLocal的弱引用作為key,如果一個ThreadLocal沒有外部強(qiáng)引用來引用它逢倍,那么系統(tǒng) GC 的時候捧颅,這個ThreadLocal勢必會被回收,這樣一來较雕,ThreadLocalMap中就會出現(xiàn)key為null的Entry碉哑,就沒有辦法訪問這些key為null的Entry的value,如果當(dāng)前線程再遲遲不結(jié)束的話亮蒋,這些key為null的Entry的value就會一直存在一條強(qiáng)引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永遠(yuǎn)無法回收扣典,造成內(nèi)存泄漏。
其實(shí)慎玖,ThreadLocalMap的設(shè)計中已經(jīng)考慮到這種情況贮尖,也加上了一些防護(hù)措施:在ThreadLocal的get(),set(),remove()的時候都會清除線程ThreadLocalMap里所有key為null的value。
但是這些被動的預(yù)防措施并不能保證不會內(nèi)存泄漏:
使用static的ThreadLocal趁怔,延長了ThreadLocal的生命周期湿硝,可能導(dǎo)致的內(nèi)存泄漏(參考ThreadLocal 內(nèi)存泄露的實(shí)例分析)。
分配使用了ThreadLocal又不再調(diào)用get(),set(),remove()方法润努,那么就會導(dǎo)致內(nèi)存泄漏关斜。
為什么使用弱引用
從表面上看內(nèi)存泄漏的根源在于使用了弱引用。網(wǎng)上的文章大多著重分析ThreadLocal使用了弱引用會導(dǎo)致內(nèi)存泄漏铺浇,但是另一個問題也同樣值得思考:為什么使用弱引用而不是強(qiáng)引用痢畜?
下面我們分兩種情況討論:
key 使用強(qiáng)引用:引用的ThreadLocal的對象被回收了,但是ThreadLocalMap還持有ThreadLocal的強(qiáng)引用鳍侣,如果沒有手動刪除丁稀,ThreadLocal不會被回收,導(dǎo)致Entry內(nèi)存泄漏倚聚。
key 使用弱引用:引用的ThreadLocal的對象被回收了线衫,由于ThreadLocalMap持有ThreadLocal的弱引用,即使沒有手動刪除秉沼,ThreadLocal也會被回收桶雀。value在下一次ThreadLocalMap調(diào)用set,get矿酵,remove的時候會被清除。
比較兩種情況矗积,我們可以發(fā)現(xiàn):由于ThreadLocalMap的生命周期跟Thread一樣長全肮,如果都沒有手動刪除對應(yīng)key,都會導(dǎo)致內(nèi)存泄漏棘捣,但是使用弱引用可以多一層保障:弱引用ThreadLocal不會內(nèi)存泄漏辜腺,對應(yīng)的value在下一次ThreadLocalMap調(diào)用set,get,remove的時候會被清除。
因此乍恐,ThreadLocal內(nèi)存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一樣長评疗,如果沒有手動刪除對應(yīng)key就會導(dǎo)致內(nèi)存泄漏,而不是因?yàn)槿跻谩?/p>
ThreadLocal 最佳實(shí)踐
綜合上面的分析茵烈,我們可以理解ThreadLocal內(nèi)存泄漏的前因后果百匆,那么怎么避免內(nèi)存泄漏呢?
每次使用完ThreadLocal呜投,都調(diào)用它的remove()方法加匈,清除數(shù)據(jù)。
在使用線程池的情況下仑荐,沒有及時清理ThreadLocal雕拼,不僅是內(nèi)存泄漏的問題,更嚴(yán)重的是可能導(dǎo)致業(yè)務(wù)邏輯出現(xiàn)問題粘招。所以啥寇,使用ThreadLocal就跟加鎖完要解鎖一樣,用完就清理洒扎。
4.總結(jié)
通過上述使用場景可以發(fā)現(xiàn)辑甜,ThreadLocal確實(shí)提供了一種編程手段,本來需要在線程中顯示聲明的局部變量逊笆,像是被ThreadLocal隱藏了起來栈戳,當(dāng)多個線程運(yùn)行起來時,每個線程都往相同的ThreadLocal中存取所需要的變量就可以了难裆,使用ThreadLocal存取的變量,就像是每個線程自己的局部變量镊掖,不受其他線程運(yùn)行狀態(tài)的影響乃戈。
通過ThreadLocal可以解決多線程讀共享數(shù)據(jù)的問題,因?yàn)楣蚕頂?shù)據(jù)會被復(fù)制到每個線程亩进,不需要加鎖便可同步訪問症虑。但ThreadLocal解決不了多線程寫共享數(shù)據(jù)的問題,因?yàn)槊總€線程寫的都是自己本線程的局部變量归薛,并沒將寫數(shù)據(jù)的結(jié)果同步到其他線程谍憔。理解了這一點(diǎn)匪蝙,才能理解所謂的:
ThreadLocal以空間換時間,提升多線程并發(fā)的效率习贫。什么意思呢逛球?每個線程都有一個ThreadLocalMap映射表,正是利用了這個映射表所占用的空間苫昌,使得多個線程都可以訪問自己的這片空間颤绕,不用擔(dān)心考慮線程同步問題,效率自然會高祟身。
ThreadLocal并不是為了解決共享數(shù)據(jù)的互斥寫問題奥务,而是通過一種編程手段,正好提供了并行讀的功能袜硫。什么意思呢氯葬?ThreadLocal并不是萬能的,它的設(shè)計初衷只是提供一個便利性婉陷,使得線程可以更為方便地使用局部變量帚称。
ThreadLocal提供了一種線程全域訪問功能,什么意思呢憨攒?一旦將一個對象添加到ThreadLocal中世杀,只要不移除它,那么肝集,在線程的生命周期內(nèi)的任何地方瞻坝,都可以通過ThreadLocal.get()方法拿到這個對象。有時候杏瞻,代碼邏輯比較復(fù)雜所刀,一個線程的代碼可能分散在很多地方,利用ThreadLocal這種便利性捞挥,就能簡化編程邏輯浮创。