1. Java基礎(1)——ThreadLocal 1.1. ThreadLocal ThreadLocal是一個泛型類,當我們在一個類中聲明一個欄位:private ThreadLocal<Foo> threadLocalFoo = new ThreadLocal<>();時,這時候,即使不同的線 ...
1. Java基礎(1)——ThreadLocal
1.1. ThreadLocal
ThreadLocal是一個泛型類,當我們在一個類中聲明一個欄位:private ThreadLocal<Foo> threadLocalFoo = new ThreadLocal<>();
時,這時候,即使不同的線程持有了該類的同一個實例,那麼它們在訪問該實例的threadLocalFoo
的時候訪問的是不同的Foo對象,這些Foo對象和這些線程是一一對應的關係,並被這些線程所私有,因此每個線程不需要對自己從threadLocalFoo
獲得的Foo實例進行加鎖(加鎖也沒用啊),這種無鎖化的設計提高了並行能力,但註意ThreadLocal並不是萬能的,有些場景可以使用ThreadLocal(比如Spring中的事務),但有些場景它的語義就是必須對同一個對象實例進行加鎖後獨占地訪問,比如單例模式,這種ThreadLocal就起不了作用了。
當然ThreadLocal還提供了initialValue
這個protected方法,用來創建聲明的泛型類型對象,因此我們還可以以下麵這種方式來聲明一個thread local:
ThreadLocal<Foo> threadLocal = new ThreadLocal<Foo>(){
@Override
protected Foo initialValue() {
return new Foo();
}
};
同時ThreadLocal還提供了一個withInitial
靜態方法,該方法接收一個相同泛型類型的Supplier,返回ThreadLocal。
Java的每個Thread實例中,都有一個ThreadLocalMap類型的實例欄位,它存放了該線程所用到過的所有ThreadLocal
式樣的實例對象,比如,有個類中聲明瞭這個欄位private ThreadLocal<Foo> threadLocalFoo = new ThreadLocal<>();
,雖然它的一個實例被多個線程持有,但這些線程不一定都訪問過這個實例的threadLocalFoo
欄位,只有訪問過這個欄位的Thread,它的thread local map中才會存Foo對象(以Entry的方式存,key為該ThreadLocal實例(共用),value為每個線程自己持有的Foo對象(私有))。
註意,我們使用ThreadLocal的是因為有些對象每個線程都可以持有一份,然後我們才使用ThreadLocal來避免同一個對象的實例方法的併發操作,但這樣的話我們要謹防ThreadLocal的退化:如果使用它的時候,用之前都是set,之後就remove,那麼相當於每訪問一次ThreadLocal都要創建出一個新的對象出來,這樣發揮不出ThreadLocal節省對象數量的作用。ThreadLocal一般被聲明為static欄位。
1.1.1. get方法
如果當前的Thread中的thread local map欄位不空,並且其中存的有對應的對象,那麼返回。
如果thread local map欄位不空,但是沒有存對應的對象,那麼使用initialValue創建對象,然後將它和該ThreadLocal實例,打包成Entry放入當前的thread local map中,返回創建的對象。
如果thread local map欄位為空,那麼首先創建對象,然後創建該線程的thread local map,然後再存Entry,再返回創建的對象。
總而言之呢,get方法就是說返回的對象都必須從當前線程的thread local map中取,thread local map沒創建,就創建thread local map,創建了但裡面沒有需要的對象,那麼就創建對象並將其塞進去,反正必須從thread local map中拿就對了。
setInitialValue方法:
createMap方法:
1.1.2. set方法
Set方法,將傳入的對象設置到當前的線程的thread local map中,註意,Entry的Key為set方法所在的ThreadLocal實例。
還是一樣,沒有thread local map就創建thread local map,反正必須塞入當前的thread local map中。
1.1.3. remove方法
remove方法,就是獲取當前線程的thread local map,如果它不空的話,就移除key為remove方法所在的ThreadLocal的Entry(不同的ThreadLocal實例對應著不同的Entry,而同一個ThreadLocal實例在一個thread local map中最多存一個,但是可以存在多個thread local map中)。
1.2. ThreadLocalMap(ThreadLocal內部類)
ThreadLocalMap是ThreadLocal機制的關鍵,它不被使用ThreadLocal的用戶所感知,它是ThreadLocal的靜態內部類,它的所有方法都是private方法,並且該類的可見性是包可見的,因此ThreadLocalMap類中的所有方法都只能被ThreadLocal的方法調用。
ThreadLocalMap的底層存儲是ThreadLocalMap.Entry
類型的數組,它的碰撞處理策略不是HashMap的開鏈法(開散列方法),而是線性探測法(linear probing,屬於閉散列方法,常見的其他閉散列方法還有:平方探測法、雙散列法)。這個線性探測法就是說:
-
在put的時候,先根據key的hash值定位到在數組中的槽位,如果對應的位置沒有Entry,那麼就可以把當前的鍵值對放入這裡,反之,如果該位置已經被占用的話,那麼需要獲取該位置的下一個位置(如果當前位置為數組最後一個位置,那麼下一個位置為0),直到找到空位為止
-
在get的時候,根據key找Entry,也是首先先根據key的hash值定位到在數組中的槽位,如果這個槽位空著,那麼說明當前map沒有存這個key,如果這個槽位不空,那麼還要檢查Entry中的key是否就是當前的key,如果不是的話還要繼續向後探測,直到遇到了空位或者遇到了key為當前key的Entry。
-
在remove的時候,首先跟get一樣,找到key對應的Entry,然後將其移除,但是移除完之後,如果該槽位後面連續的槽位也都被占用了,那麼還要對這些槽位中的Entry再進行位置修正。
和Map介面中的Entry不一樣,ThreadLocalMap.Entry
聲明為:
ThreadLocalMap.Entry
是一個對ThreadLocal對象的弱引用,也就是說,雖然該Entry會持有ThreadLocal對象,但是並不會影響該ThreadLocal對象的GC,而這個弱引用對象Entry本身是個尋常的Java對象,它還持有了ThreadLocal的泛型類型的對象(比如上面例子中的Foo),這個持有關係是強引用,只有當ThreadLocalMap的底層數組不再持有這個Entry時,該Entry才會被GC。因此,也就是說,如果ThreadLocalMap如果不做特殊處理的話,那麼即使是ThreadLocal實例都被GC了,但是它們對應的Entry依舊無法被GC,導致實際使用的泛型類型對象也無法被GC,只是這些Entry引用的ThreadLocal變成null了,這個問題其實就是記憶體泄露。
為瞭解決這個記憶體泄露問題,ThreadLocalMap線上性探測操作中,如果發現了持有的thread local已經被GC的Entry(Stale Entry),那麼就不再持有這個Entry,使得這個Entry可以被GC,但是即使這樣依然無法完全保證stale entry都能及時的被清理,這個殘留的問題就是偽記憶體泄露問題。
這個偽記憶體泄露問題一般存在於線程池的場景下,因為如果線程本身被銷毀,那麼thread local map也會銷毀,也不存在什麼泄露問題。
為瞭解決這個偽記憶體泄露問題,我們作用應用程式的開發者,在使用到threadlocal時,如果我們不再需要它時,那麼就要手動進行remove操作,使得對應的Entry可以被GC。
這個Entry數組初始容量為16,threshold為當前數組長度的三分之二(hard code),每次向Thread local map放入entry之後,會檢查更新後的size(數組中的Entry數量)是否達到了threshold,如果達到了,那麼就需要進行擴容,擴容的邏輯是,先把所有stale entry清理後,判斷清理的數量是否達到了四分之一threshold,如果是,那麼說明當前thread local map只是因為stale entry太多的緣故導致的容量緊張,就只需執行清理動作,而不用將底層數組容量翻倍併進行entry的遷移,這個策略的目的:
-
數組容量翻倍本身占用空間,並且擴容時搬運entry的操作相對相不擴容清理stale entry的操作來說開銷更大。
-
更好的去抑制上面講的偽記憶體泄露問題。
註意,thread local map底層的Entry數組只會擴容,不會縮容。
1.2.1. 構造函數
1.2.2. getEntry方法
getEntryAfterMiss:
getEntryAfterMiss就是get操作的線性探測步驟。
expungeStaleEntry:
這個expungeStaleEntry就是說呢,需要刪除那些Stale的Entry(已經被GC後的ThreadLocal實例對應的Entry)。
它不止刪除給定stale位置的entry,它還有線性探測該位置之後被連續占用的位置的entry。在這些entry中,對於不是stale的,我們需要把它們挪到更正後的位置上,對於是stale的,將其刪除。
expungeStaleEntries:
expungeStaleEntries方法就是遍曆數組中的所有Entry,檢查是否stale,如果stale,那麼調用expungeStaleEntry來刪除並調整。
1.2.3. set方法
Set方法往thread local map中添加一個Entry。
如果該Entry未經線性探測時的位置未被占用,那麼直接占用,更新size計數,並且從該位置嘗試清理一些stale entry(見cleanSomeSlots方法)。如果清理成功,那麼此時size鐵定沒有超出threshold(因為此時至少清理了一個Entry,而set方法一次只set一個,並且初始情況下size小於threshold)。如果沒有清理到到,那麼就判斷更新後的size是否超過了threshold,如果超過了,那麼要擴容。
如果原始位置被占用了,那麼就需要通過線性探測,探測之後的位置,在探測過程中:
-
如果發現已經有給定的Key的Entry了,那麼直接替換value就完事了。
-
如果沒有發現stale entry,那麼就將遇到的第一個空位用來放置該Entry,然後完事,此時同樣需要像上面一樣嘗試清理stale entry,如果清理失敗看需不需要擴容等。
-
如果在探測中發現了stale entry,那麼就進行替換操作,註意這個替換操作很複雜,見replaceStaleEntry方法。
replaceStaleEntry:
前兩個參數是需要放置的Entry的信息,最後一個參數是stale entry的位置。
首先是向前探測,因為給的stale entry的位置可能是處於一個連續被占用段的中間,因此來向前探測,來找到該連續占用段的第一個stale位置。
然後再從給定的stale位置向後探測,在這個向後探測的過程中:
-
如果遇到了跟傳入key對應的Entry,那麼就將該Entry給挪到傳入的stale位置。如果上一步向前探測時沒有找到stale entry,那麼就從當前的位置向後回收連續占用段的stale entry;如果向前探測時找到了的話,就從這個找到的位置向後回收本連續占用段的stale entry。
-
如果沒有遇到該key對應的Entry,並且之前向前探測的時候也沒有找到當前連續占用段的第一個stale位置,那麼就需要在這個向後探測從保存第一個stale entry的位置,探測結束後將傳入的stale位置放入entry,然後從這個向後探測過程中保存的stale位置開始向後回收所在連續占用段的stale entry。
上面兩種情況結束後,如果它們expungeStaleEntry的開始位置不是傳入的stale位置,那麼在這個expungeStaleEntry操作的結束位置(這個結束位置是一個空位)的下一位置開始向後嘗試回收一些stale entry,見cleanSomeSlots方法。
cleanSomeSlots:
這個方法的作用是說從給定position(不包含該position)開始向後找stale entry,如果連續找了 log(n) 個位置都不是stale entry,那麼就結束,反之如果找到一個stale entry的話,那麼需要再重新向後看 log(len) 個位置。
註意,這個方法在set方法、replaceStaleEntry方法中的末尾都有調用,區別在於,set方法中調用cleanSomeSlots時設置初始初始向後看的位置數目為log(size),而replaceStaleEntry設置的是log(len)。
rehash:
先把所有stale entry清理後,判斷清理的數量是否達到了四分之一threshold,如果是,那麼說明當前thread local map只是因為stale entry太多的緣故導致的容量緊張,就只需執行清理動作,而不用將底層數組容量翻倍併進行entry的遷移。
1.2.4. remove方法
1.3. ThreadLocal記憶體泄露
記憶體泄露(Memory Leak)指由於對象永遠無法被垃圾回收導致其占用的Java虛擬機記憶體無法被釋放。持續的記憶體泄露會導致Java虛擬機可用記憶體主鍵減少,並最終可能導致Java記憶體溢出(OOM),直到Java虛擬機宕機。
偽記憶體泄露(Memory Psedo-leak)類似於記憶體泄露,偽記憶體泄露中對象占用的記憶體在其不再被使用的相當長時間內仍然無法回收,甚至永遠無法回收。就是說,偽記憶體泄露的對象,理論上將是可以被回收的,但是這個等待回收的時間太長了。
談及ThreadLocal map的時候,我們談到了,當使用threadlocal任務不進行remove操作,並且任務又線上程池中運行時,有偽記憶體泄露的風險,這個風險被thread local map本身的實現抑制了,但是仍然存在,解決的辦法就是即使使用remove操作。
此外還有一種更加嚴重的記憶體泄露:每個線程實例持有thread local map,然後間接持有了線程特有對象(thread local的泛型類型),在Tomcat環境下,Web應用(打包成WAR)自身定義的類由類載入器WebAppClassLoader負責載入, JDK的標準類由類載入器StandardClassLoader負責載入。不管類每個類被哪個載入器載入,它都持有了載入它的載入器的引用,除了最特殊的那個。對於WebAppClassLoader來說,它還會持有它載入過的所有class的引用,這樣就導致,如果如果某個由WebAppClassLoader載入的類型(假設為ThreadLocalMemoryLeak)有個靜態的ThreadLocal欄位(threadLocalFoo),那麼該線程特有對象(foo對象)會持有該對象的Class對象(Foo.class),Foo類型會持有WebAppClassLoader,WebAppClassLoader又會持有ThreadLocalMemoryLeak的Class對象,這個Class對象又持有了threadLocalFoo這個靜態欄位,也就是說,foo對象這個線程特有對象,最終又反過來持有ThreadLocal實例了,這就導致,如果不及時remove的話,那麼thread local map中的Entry永遠不會stale,即使這個Web app不運行了,但是Tomcat容器還在運行的話,由於底層的這些線程不會被銷毀,因此thread local就產生了記憶體泄露,更進一步講Foo類的Class對象、ThreadLocalMemoryLeak的Class對象,以及它們的靜態變數所引用的所有對象,都無法被回收。當然Tomcat提供了一套記憶體泄露的檢查機制以及一定程度的自動規避,但我們不要依賴這個機制。為瞭解決這個問題,我們要及時remove。
作者: 邁吉
出處: https://www.cnblogs.com/stepfortune/
關於作者:邁吉
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出, 原文鏈接 如有問題, 可郵件([email protected])咨詢.