Java后端技術(shù) 2017-10-21 ThreadLocal可以說是筆試面試的???,每逢面試基本都會(huì)問到,關(guān)于ThreadLocal的原理以及不正當(dāng)?shù)氖褂迷斐傻腛OM內(nèi)存溢出的問題,值得花時(shí)間仔細(xì)研究一下其原理。這一篇主要學(xué)習(xí)一下ThreadLocal的原理,在下一篇會(huì)深入理解一下OOM內(nèi)存溢出的原理和最佳實(shí)踐。
ThreadLocal很容易讓人望文生義,想當(dāng)然地認(rèn)為是一個(gè)“本地線程”。其實(shí),ThreadLocal并不是一個(gè)Thread,而是Thread的一個(gè)局部變量,也許把它命名為更容易讓人理解一些。 當(dāng)使用ThreadLocal維護(hù)變量時(shí),ThreadLocal為每個(gè)使用該變量的線程提供獨(dú)立的變量副本,所以每一個(gè)線程都可以獨(dú)立地改變自己的副本,而不會(huì)影響其它線程所對(duì)應(yīng)的副本。 ThreadLocal 的作用是提供線程內(nèi)的局部變量,這種變量在線程的生命周期內(nèi)起作用,減少同一個(gè)線程內(nèi)多個(gè)函數(shù)或者組件之間一些公共變量的傳遞的復(fù)雜度。 從線程的角度看,目標(biāo)變量就像是線程的本地變量,這也是類名中“Local”所要表達(dá)的意思。 一、ThreadLocal全部方法和內(nèi)部類 ThreadLocal全部方法和內(nèi)部類結(jié)構(gòu)如下:
ThreadLocal公有的方法就四個(gè),分別為:get、set、remove、intiValue:
也就是說我們平時(shí)使用的時(shí)候關(guān)心的是這四個(gè)方法。 ThreadLocal是如何做到為每一個(gè)線程維護(hù)變量的副本的呢? 其實(shí)實(shí)現(xiàn)的思路很簡(jiǎn)單:在ThreadLocal類中有一個(gè)static聲明的Map,用于存儲(chǔ)每一個(gè)線程的變量副本,Map中元素的鍵為線程對(duì)象,而值對(duì)應(yīng)線程的變量副本。我們自己就可以提供一個(gè)簡(jiǎn)單的實(shí)現(xiàn)版本:
運(yùn)行結(jié)果:
雖然上面的代碼清單中的這個(gè)ThreadLocal實(shí)現(xiàn)版本顯得比較簡(jiǎn)單粗,但其目的主要在與呈現(xiàn)JDK中所提供的ThreadLocal類在實(shí)現(xiàn)上的思路。 二、ThreadLocal源碼分析 1、線程局部變量在Thread中的位置 既然是線程局部變量,那么理所當(dāng)然就應(yīng)該存儲(chǔ)在自己的線程對(duì)象中,我們可以從 Thread 的源碼中找到線程局部變量存儲(chǔ)的地方:
我們可以看到線程局部變量是存儲(chǔ)在Thread對(duì)象的屬性中,而屬性是一個(gè)對(duì)象。 ThreadLocalMap為ThreadLocal的靜態(tài)內(nèi)部類,如下圖所示:
2、Thread和ThreadLocalMap的關(guān)系 Thread和ThreadLocalMap的關(guān)系,先看下邊這個(gè)簡(jiǎn)單的圖,可以看出Thread中的就是ThreadLocal中的ThreadLocalMap:
到這里應(yīng)該大致能夠感受到上述三者之間微妙的關(guān)系,再看一個(gè)復(fù)雜點(diǎn)的圖:
可以看出每個(gè)實(shí)例都有一個(gè)。在上圖中的一個(gè)Thread的這個(gè)ThreadLocalMap中分別存放了3個(gè)Entry,默認(rèn)一個(gè)ThreadLocalMap初始化了16個(gè)Entry,每一個(gè)Entry對(duì)象存放的是一個(gè)ThreadLocal變量對(duì)象。 再看一張網(wǎng)絡(luò)上的圖片,應(yīng)該可以更好的理解,如下圖:
這里的Map其實(shí)是ThreadLocalMap。 3、ThreadLocalMap與WeakReference 從字面上就可以看出這是一個(gè)保存對(duì)象的map(其實(shí)是以它為Key),不過是經(jīng)過了兩層包裝的ThreadLocal對(duì)象: (1)第一層包裝是使用將對(duì)象變成一個(gè)弱引用的對(duì)象; (2)第二層包裝是定義了一個(gè)專門的類 Entry 來擴(kuò)展: 類 Entry 很顯然是一個(gè)保存map鍵值對(duì)的實(shí)體,為key, 要保存的線程局部變量的值為。調(diào)用的的構(gòu)造函數(shù),表示將對(duì)象轉(zhuǎn)換成弱引用對(duì)象,用做key。
4、ThreadLocalMap 的構(gòu)造函數(shù) 可以看出,ThreadLocalMap這個(gè)map的實(shí)現(xiàn)是使用一個(gè)數(shù)組來保存鍵值對(duì)的實(shí)體,初始大小為16,自己實(shí)現(xiàn)了如何從到 value 的映射:
使用一個(gè)的原子屬性,通過每次增加,然后取得在數(shù)組中的索引。
總的來說,ThreadLocalMap是一個(gè)類似HashMap的集合,只不過自己實(shí)現(xiàn)了尋址,也沒有HashMap中的put方法,而是set方法等區(qū)別。 三、ThreadLocal的set方法 由于每個(gè)thread實(shí)例都有一個(gè)ThreadLocalMap,所以在進(jìn)行set的時(shí)候,首先根據(jù)Thread.currentThread()獲取當(dāng)前線程,然后根據(jù)當(dāng)前線程t,調(diào)用getMap(t)獲取ThreadLocalMap對(duì)象, 如果是第一次設(shè)置值,ThreadLocalMap對(duì)象是空值,所以會(huì)進(jìn)行初始化操作,即調(diào)用方法:
即是調(diào)用上述的構(gòu)造方法進(jìn)行構(gòu)造,這里僅僅是初始化了16個(gè)元素的引用數(shù)組,并沒有初始化16個(gè) Entry 對(duì)象。而是一個(gè)線程中有多少個(gè)線程局部對(duì)象要保存,那么就初始化多少個(gè) Entry 對(duì)象來保存它們。
到了這里,我們可以思考一下,為什么要這樣實(shí)現(xiàn)了。 1、為什么要用 ThreadLocalMap 來保存線程局部對(duì)象呢? 原因是一個(gè)線程擁有的的局部對(duì)象可能有很多,這樣實(shí)現(xiàn)的話,那么不管你一個(gè)線程擁有多少個(gè)局部變量,都是使用同一個(gè) ThreadLocalMap 來保存的,ThreadLocalMap 中的初始大小是16。超過容量的2/3時(shí),會(huì)擴(kuò)容。 然后在回到如果map不為空的情況,會(huì)調(diào)用方法,我們看到是以當(dāng)前 thread 的引用為 key, 獲得,然后調(diào)用保存進(jìn):
可以看到,方法為每個(gè)Thread對(duì)象都創(chuàng)建了一個(gè)ThreadLocalMap,并且將value放入ThreadLocalMap中,ThreadLocalMap作為Thread對(duì)象的成員變量保存。那么可以用下圖來表示ThreadLocal在存儲(chǔ)value時(shí)的關(guān)系。
2、了解了set方法的大致原理之后,我們?cè)谘芯恳欢纬绦蛉缦拢?/strong>
這樣的話就相當(dāng)于一個(gè)線程依附了三個(gè)ThreadLocal對(duì)象,執(zhí)行完最后一個(gè)set方法之后,調(diào)試過程如下:
可以看到table(Entry集合)中有三個(gè)對(duì)象,對(duì)象的值就是我們?cè)O(shè)置的三個(gè)threadLocal的對(duì)象值; 3、如果在修改一下代碼,修改為兩個(gè)線程:
這樣的話,可以看到運(yùn)行調(diào)試圖如下: 然后更改到Thread2,查看,由于多線程,線程1運(yùn)行到上圖情況,線程2運(yùn)行到下圖情況,也可以看出他們是不同的ThreadLocalMap:
那如果多個(gè)線程,只設(shè)置一個(gè)ThreadLocal變量那,結(jié)果可想而知,這里不再贅述!
另外,有一點(diǎn)需要提示一下,代碼如下:
運(yùn)行結(jié)果:
可以看到,在這個(gè)線程中的ThreadLocal變量的值始終是只有一個(gè)的,即以前的值被覆蓋了的!這里是因?yàn)镋ntry對(duì)象是以該ThreadLocal變量的引用為key的,所以多次賦值以前的值會(huì)被覆蓋,特此注意! 到這里應(yīng)該可以清楚了的了解Thread、ThreadLocal和ThreadLocalMap之間的關(guān)系了! 四、ThreadLocal的get方法
經(jīng)過上述set方法的分析,對(duì)于get方法應(yīng)該理解起來輕松了許多,首先獲取ThreadLocalMap對(duì)象,由于ThreadLocalMap使用的當(dāng)前的ThreadLocal作為key,所以傳入的參數(shù)為this,然后調(diào)用方法,通過這個(gè)key構(gòu)造索引,根據(jù)索引去table(Entry數(shù)組)中去查找線程本地變量,根據(jù)下邊找到Entry對(duì)象,然后判斷Entry對(duì)象e不為空并且e的引用與傳入的key一樣則直接返回,如果找不到則調(diào)用方法。調(diào)用表示直接散列到的位置沒找到,那么順著hash表遞增(循環(huán))地往下找,從i開始,一直往下找,直到出現(xiàn)空的槽為止。
五、ThreadLocal的內(nèi)存回收 ThreadLocal 涉及到的兩個(gè)層面的內(nèi)存自動(dòng)回收: (1)在 ThreadLocal 層面的內(nèi)存回收: 當(dāng)線程死亡時(shí),那么所有的保存在的線程局部變量就會(huì)被回收,其實(shí)這里是指線程Thread對(duì)象中的會(huì)被回收,這是顯然的。
(2)ThreadLocalMap 層面的內(nèi)存回收: 如果線程可以活很長(zhǎng)的時(shí)間,并且該線程保存的線程局部變量有很多(也就是 Entry 對(duì)象很多),那么就涉及到在線程的生命期內(nèi)如何回收 ThreadLocalMap 的內(nèi)存了,不然的話,Entry對(duì)象越多,那么ThreadLocalMap 就會(huì)越來越大,占用的內(nèi)存就會(huì)越來越多,所以對(duì)于已經(jīng)不需要了的線程局部變量,就應(yīng)該清理掉其對(duì)應(yīng)的Entry對(duì)象。
使用的方式是,Entry對(duì)象的key是WeakReference 的包裝,當(dāng)ThreadLocalMap 的,已經(jīng)被占用達(dá)到了三分之二時(shí)(也就是線程擁有的局部變量超過了10個(gè)) ,就會(huì)嘗試回收 Entry 對(duì)象,我們可以看到方法中有下面的代碼: cleanSomeSlots 就是進(jìn)行回收內(nèi)存:
六、ThreadLocal可能引起的OOM內(nèi)存溢出問題簡(jiǎn)要分析 我們知道ThreadLocal變量是維護(hù)在Thread內(nèi)部的,這樣的話只要我們的線程不退出,對(duì)象的引用就會(huì)一直存在。當(dāng)線程退出時(shí),Thread類會(huì)進(jìn)行一些清理工作,其中就包含ThreadLocalMap,Thread調(diào)用exit方法如下: 但是,當(dāng)我們使用線程池的時(shí)候,就意味著當(dāng)前線程未必會(huì)退出(比如固定大小的線程池,線程總是存在的)。如果這樣的話,將一些很大的對(duì)象設(shè)置到ThreadLocal中(這個(gè)很大的對(duì)象實(shí)際保存在Thread的threadLocals屬性中),這樣的話就可能會(huì)出現(xiàn)內(nèi)存溢出的情況。
一種場(chǎng)景就是說如果使用了線程池并且設(shè)置了固定的線程,處理一次業(yè)務(wù)的時(shí)候存放到ThreadLocalMap中一個(gè)大對(duì)象,處理另一個(gè)業(yè)務(wù)的時(shí)候,又一個(gè)線程存放到ThreadLocalMap中一個(gè)大對(duì)象,但是這個(gè)線程由于是線程池創(chuàng)建的他會(huì)一直存在,不會(huì)被銷毀,這樣的話,以前執(zhí)行業(yè)務(wù)的時(shí)候存放到ThreadLocalMap中的對(duì)象可能不會(huì)被再次使用,但是由于線程不會(huì)被關(guān)閉,因此無法釋放Thread 中的ThreadLocalMap對(duì)象,造成內(nèi)存溢出。 也就是說,ThreadLocal在沒有線程池使用的情況下,正常情況下不會(huì)存在內(nèi)存泄露,但是如果使用了線程池的話,就依賴于線程池的實(shí)現(xiàn),如果線程池不銷毀線程的話,那么就會(huì)存在內(nèi)存泄露。所以我們?cè)谑褂镁€程池的時(shí)候,使用ThreadLocal要格外小心! 七、總結(jié) 通過源代碼可以看到每個(gè)線程都可以獨(dú)立修改屬于自己的副本而不會(huì)互相影響,從而隔離了線程和線程.避免了線程訪問實(shí)例變量發(fā)生安全問題. 同時(shí)我們也能得出下面的結(jié)論: (1)ThreadLocal只是操作Thread中的ThreadLocalMap對(duì)象的集合; (2)ThreadLocalMap變量屬于線程的內(nèi)部屬性,不同的線程擁有完全不同的ThreadLocalMap變量; (3)線程中的ThreadLocalMap變量的值是在ThreadLocal對(duì)象進(jìn)行set或者get操作時(shí)創(chuàng)建的; (4)使用當(dāng)前線程的ThreadLocalMap的關(guān)鍵在于使用當(dāng)前的ThreadLocal的實(shí)例作為key來存儲(chǔ)value值; (5) ThreadLocal模式至少?gòu)膬蓚€(gè)方面完成了數(shù)據(jù)訪問隔離,即縱向隔離(線程與線程之間的ThreadLocalMap不同)和橫向隔離(不同的ThreadLocal實(shí)例之間的互相隔離); (6)一個(gè)線程中的所有的局部變量其實(shí)存儲(chǔ)在該線程自己的同一個(gè)map屬性中; (7)線程死亡時(shí),線程局部變量會(huì)自動(dòng)回收內(nèi)存; (8)線程局部變量時(shí)通過一個(gè) Entry 保存在map中,該Entry 的key是一個(gè) WeakReference包裝的ThreadLocal, value為線程局部變量,key 到 value 的映射是通過:來完成的; (9)當(dāng)線程擁有的局部變量超過了容量的2/3(沒有擴(kuò)大容量時(shí)是10個(gè)),會(huì)涉及到ThreadLocalMap中Entry的回收; 對(duì)于多線程資源共享的問題,同步機(jī)制采用了“以時(shí)間換空間”的方式,而ThreadLocal采用了“以空間換時(shí)間”的方式。前者僅提供一份變量,讓不同的線程排隊(duì)訪問,而后者為每一個(gè)線程都提供了一份變量,因此可以同時(shí)訪問而互不影響。 參考文章: 2、http://www.cnblogs.com/digdeep/p/4510875.html |
|