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])咨詢.
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/481142.html
標籤:Java
