天天看點

多線程 - ThreadLocal 是什麼?有哪些使用場景?

ThreadLocal常用API

void set (Object value)設定目前線程的線程局部變量的值。

public Object get() 該方法傳回目前線程所對應的線程局部變量。

public void remove() 将目前線程局部變量的值删除,目的是為了減少記憶體的占用,該方法是 JDK 5.0 新增的方法。需要指出的是,當線程結束後,對應該線程的局部變量将自動被垃圾回收,是以顯式調用該方法清除線程的局部變量并不是必須的操作,但它 可以加快記憶體回收的速度。無論是 get()、set()在某些時候,調用了 expungeStaleEntry 方法用來清除 Entry 中 Key 為 null 的 Value,但是 這是不及時的,也不是每次都會執行的,是以一些情況下還是會發生記憶體洩露。 隻有 remove()方法中顯式調用了 expungeStaleEntry 方法。

protected Object initialValue() 傳回該線程局部變量的初始值,該方法是一個 protected 的方法,顯然是為 了讓子類覆寫而設計的。這個方法是一個延遲調用方法,線上程第 1 次調用 get() 或 set(Object)時才執行,并且僅執行 1 次。ThreadLocal 中的預設實作直接傳回一 個 null。

public final static ThreadLocal<String> RESOURCE = new ThreadLocal<String>();RESOURCE 代表一個能夠存放 String 類型的 ThreadLocal 對象。 此時不論什麼一個線程能夠并發通路這個變量,對它進行寫入、讀取操作,都是線程安全的。

為什麼說“get()、set()在某些時候,調用了 expungeStaleEntry 方法用來清除 Entry 中 Key 為 null 的 Value,但是 這是不及時的”?

【這裡寫的不确定,請大家斧正】以get()為例,隻有當被置為null的ThreadLocal自己調用get時,才會走入getEntryAfterMiss()的邏輯。其他ThreadLocal變量調用get不會清除。

ThreadLocal适用場景

ThreadLocal在spring的事務管理,包括Hibernate的session管理等都有出現,在web開發中,有時會用來管理使用者會話 HttpSession,web互動中這種典型的“一請求一線程”的場景似乎比較适合使用ThreadLocal,但是需要特别注意的是,由于此時session與線程關聯,而tomcat這些web伺服器多會采用線程池機制,也就是說線程是可複用的,是以在每一次進入的時候都需要重新進行set,或者在結束時及時remove。

ThreadLocal底層實作原理

重點:記住這張圖!!!!

Entry裡的key儲存的是Threadlocal的弱引用。

Entry裡的value儲存的是強引用,儲存的也是引用,不是執行個體。

多線程 - ThreadLocal 是什麼?有哪些使用場景?

get 方法,其實就是拿到每個線程獨有的 ThreadLocalMap,然後再用 ThreadLocal 的目前執行個體,拿到 Map 中的相應的 Entry,然後就可 以拿到相應的值傳回出去。當然,如果 Map 為空,還會先進行 map 的建立,初 始化等工作。 

多線程 - ThreadLocal 是什麼?有哪些使用場景?

上面先取到目前線程,然後調用 getMap 方法擷取對應的 ThreadLocalMap, ThreadLocalMap 是 ThreadLocal 的靜态内部類,然後 Thread 類中有一個這樣類型 成員,是以 getMap 是直接傳回 Thread 的成員。

看下 ThreadLocal 的内部類 ThreadLocalMap 源碼: 

多線程 - ThreadLocal 是什麼?有哪些使用場景?

可以看到有個 Entry 内部靜态類,它繼承了 WeakReference,總之它記錄了 兩個資訊,一個是 ThreadLocal<?>類型,一個是 Object 類型的值。getEntry 方法 則是擷取某個 ThreadLocal 對應的值,set 方法就是更新或指派相應的 ThreadLocal 對應的值。

ThreadLocal引發的記憶體洩漏

ThreadLocal 記憶體洩漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一樣長,如果沒有手動删除對應 key 就會導緻記憶體洩漏,而不是因為弱引 用。 

JVM 利用設定 ThreadLocalMap 的 Key 為弱引用,來避免記憶體洩露。

JVM 利用調用 remove、get、set 方法的時候,回收弱引用。 

當 ThreadLocal 存儲很多 Key 為 null 的 Entry 的時候,而不再去調用 remove、 get、set 方法,那麼将導緻記憶體洩漏。

使用線程池+ ThreadLocal 時要小心,因為這種情況下,線程是一直在不斷的 重複運作的,進而也就造成了 value 可能造成累積的情況。

根據我們前面對 ThreadLocal 的分析,我們可以知道每個 Thread 維護一個 ThreadLocalMap,這個映射表的 key 是 ThreadLocal 執行個體本身(應該是 ThreadLocal 執行個體的虛引用),value 是真正需 要存儲的 Object,也就是說 ThreadLocal 本身并不存儲值,它隻是作為一個 key 來讓線程從 ThreadLocalMap 擷取 value。仔細觀察 ThreadLocalMap,這個 map 是使用ThreadLocal的弱引用作為 Key 的,弱引用的對象在 GC 時會被回收。 是以使用了 ThreadLocal 後,引用鍊如圖所示:

多線程 - ThreadLocal 是什麼?有哪些使用場景?

圖中的虛線表示弱引用。

這樣,當把 threadlocal 變量(Thread Local Ref)置為 null 以後,沒有任何強引用指向 threadlocal 執行個體(紅色方塊 Thread Local),是以 threadlocal執行個體将會被 gc 回收。這樣一來,ThreadLocalMap 中就會出現

key 為 null 的 Entry,就沒有辦法通路這些 key 為 null 的 Entry 的 value,如果目前 線程再遲遲不結束的話,這些 key 為 null 的 Entry 的 value 就會一直存在一條強 引用鍊:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,而這塊 value 永 遠不會被通路到了,是以存在着記憶體洩露。

隻有目前 thread 結束以後,current thread 就不會存在棧中,強引用斷開, Current Thread、Map value 将全部被 GC 回收。最好的做法是不在需要使用 ThreadLocal 變量後,都調用它的 remove()方法,清除資料。

是以回到我們前面的實驗場景,場景 3 中,雖然線程池裡面的任務執行完畢 了,但是線程池裡面的 5 個線程會一直存在直到 JVM 退出,我們 set 了線程的 localVariable 變量後沒有調用 localVariable.remove()方法,導緻線程池裡面的 5 個 線程的 threadLocals 變量裡面的 new LocalVariable()執行個體沒有被釋放。

其實考察 ThreadLocal 的實作,我們可以看見,無論是 get()、set()在某些時 候,調用了 expungeStaleEntry 方法用來清除 Entry 中 Key 為 null 的 Value,但是 這是不及時的,也不是每次都會執行的,是以一些情況下還是會發生記憶體洩露。 隻有 remove()方法中顯式調用了 expungeStaleEntry 方法。

從表面上看記憶體洩漏的根源在于使用了弱引用,但是另一個問題也同樣值得 思考:為什麼使用弱引用而不是強引用? 

從表面上看記憶體洩漏的根源在于使用了弱引用,但是另一個問題也同樣值得 思考:為什麼使用弱引用而不是強引用。

下面我們分兩種情況讨論:

key 使用強引用:對 ThreadLocal 對象執行個體的引用被置為 null 了,但是 ThreadLocalMap 還持有這個 ThreadLocal 對象執行個體的強引用,如果沒有手動删除, ThreadLocal 的對象執行個體不會被回收,導緻 Entry 記憶體洩漏。

key 使用弱引用:對 ThreadLocal 對象執行個體的引用被被置為 null 了,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使沒有手動删除,ThreadLocal 的 對象執行個體也會被回收。value 在下一次 ThreadLocalMap 調用 set,get,remove 都 有機會被回收。

比較兩種情況,我們可以發現:由于 ThreadLocalMap 的生命周期跟 Thread 一樣長,如果都沒有手動删除對應 key,都會導緻記憶體洩漏,但是使用弱引用可 以多一層保障。

是以,ThreadLocal 記憶體洩漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一樣長,如果沒有手動删除對應 key 就會導緻記憶體洩漏,而不是因為弱引 用。 

錯誤使用 ThreadLocal 導緻線程不安全

錯誤原因:Entry裡的value儲存的是強引用,儲存的也是引用,不是執行個體。是以不同的線程在ThreadLocalMap儲存的還是引用。由于這裡儲存的是一個static的變量,是以不同的線程儲存的value引用都指向同一個對象。

修改方法:把number的static去掉。

多線程 - ThreadLocal 是什麼?有哪些使用場景?

詳細解釋:

為什麼每個線程都輸出 5? 難道他們沒有獨自儲存自己的 Number 副本嗎? 為什麼其他線程還是能夠修改這個值?

仔細考察 ThreadLocal 和 Thead 的代碼, 我們發現 ThreadLocalMap 中儲存的其實是對象的一個引用,這樣的話,當有其 他線程對這個引用指向的對象執行個體做修改時,其實也同時影響了所有的線程持有 的對象引用所指向的同一個對象執行個體。這也就是為什麼上面的程式為什麼會輸出 一樣的結果:5 個線程中儲存的是同一 Number 對象的引用,線上程睡眠的時候, 其他線程将 num 變量進行了修改,而修改的對象 Number 的執行個體是同一份,因 此它們最終輸出的結果是相同的。

而上面的程式要正常的工作,應該的用法是讓每個線程中的 ThreadLocal 都 應該持有一個新的 Number 對象。 

ThreadLocal在Spring事務管理中的應用 

适用ThreadLocal。。。TODO: find answer from ppt、MP4

繼續閱讀