最近接了一個新需求,業務場景上需要在原有基礎上新增2個欄位,介面新增引數意味著很多類和方法的邏輯都需要改變,需要先判斷是否屬於該業務場景,再做對應的邏輯。原本的打算是在入口處新增變數,在運算元據的時候進行邏輯判斷將變數進行儲存或查詢。
如果全鏈路都變更入參和結構,很明顯程式碼上很不優雅,後續如果還要增加業務場景,又需要再改一遍。如果有一個方法可以傳遞全域性變數,而且僅限於當前執行緒就好了。
到此,會想到有兩種解決方案:之前用的比較少的ThreadLocal或者使用redis快取。考慮到新增欄位都是些增刪改查的操作,沒有必要存到redis中,故使用ThreadLocal。
以微服務架構為例,服務提供方在收到呼叫方的請求後,會把這個請求分配給一個執行緒進行處理。一般來說,一個請求會一直由同一個執行緒處理,中間不會切換執行緒,所以如果有一個執行緒中共用的變數,可以當全域性變數使用。
ThreadLocal實現的就是一個執行緒中的全域性變數,與真正的全域性變數的區別在於ThreadLocal的變數是每個執行緒中的全域性變數,也就是說不同執行緒存取到的值是不一樣的。其填充的變數屬於當前執行緒,該變數對於其他執行緒是隔離的。
由定義可以發現,ThreadLocal有兩個特性:每個Thread的變數只能由當前Thread使用;由於其他執行緒不可存取,則不存在多執行緒間共用的問題。
ThreadLocal提供了執行緒原生的範例,它與普通變數的區別在於,每個使用該變數的執行緒都會初始化一個完全獨立的範例副本。
ThreadLocal變數通常被private static修飾,這樣的好處是當一個執行緒結束時,它所使用的ThreadLocal範例副本都可被回收,避免重複建立。壞處就是這樣做可能正好導致記憶體漏失。
ThreadLocal最樸素的內部實現是Map<threadlocal, Object>,這是一個HashMap,又稱為ThreadLocalMap。但Java原始碼並不是Map<threadlocal, Object>的實現。這是因為如果多個執行緒存取同一個map,這個map需要是執行緒安全的,構造比較麻煩。Java採用了更簡單粗暴的做法:每個執行緒都有自己的ThreadLocal專屬map,裡面可以存放多個ThreadLocal變數,這樣就解決了多執行緒同時操作一個map帶來的多執行緒並行問題。
因為要把ThreadLocal的變數當做全域性變數使用,需要把變數與初始化函數寫在通用的類中,如DDD領域模型中寫在Common模組。
具體的實現如下:
public class ThreadLocalUtil {
private static ThreadLocal<Integer> THREAD_LOCAL = new ThreadLocal<>();
public static Integer getScene() {
return THREAD_LOCAL.get();
}
public static void initScene(Integer scene) {
if (THREAD_LOCAL == null) {
THREAD_LOCAL = new ThreadLocal<>();
}
THREAD_LOCAL.set(scene);
}
public static void remove() {
THREAD_LOCAL.remove();
}
}
上面提到了的ThreadLocal會帶來記憶體洩露的問題,深入分析下:
一個ThreadLocal範例對應當前執行緒的一個物件範例,如果把ThreadLocal宣告為某個類的範例變數不是靜態變數,那麼每次建立一個該類的範例就會導致一個新的物件範例被建立。而這些被建立的範例是同一個類的範例,於是同一個執行緒可能會存取到同一個類的不同範例,這即使不會導致錯誤,也會導致重複建立同樣的物件。如果使用static修飾後,只要相應的類沒有被垃圾回收掉,那麼這個類就會持有相對應的ThreadLocal範例參照。
ThreadLocal自身並不儲存值,而是作為一個key來讓執行緒從ThreadLocal中獲取value。ThreadLocalMap中的key是弱參照,所以jvm在垃圾回收時如果外部沒有強參照來參照它,ThreadLocal必然會被回收。但是,作為ThreadLocalMap中的key,ThreadLocal被回收後,ThreadLocalMap就會存在null,但value卻不為null。如果當前執行緒一直不結束或者執行緒結束後不被你銷燬,這會產生記憶體洩露(已分配空間的堆記憶體由於某種原因未釋放或無法釋放導致系統記憶體浪費或程式執行變慢甚至系統奔潰)。
因此,key弱參照並不是導致記憶體洩露的原因,而是因為ThreadLocalMap的生命週期與當前執行緒一樣長,並且沒有手動刪除對應的value。
解決的方法也很簡單,只需要打破參照路徑中的ThreadLocalMap對物件範例的參照即可。也就是在使用完ThreadLocal之後,必須呼叫ThreadLocal.remove()。
為什麼要將Map中的key設定為弱參照呢?
實際上,設定key為弱參照能預防大多數記憶體洩露的情況。如果key使用強參照,參照的ThreadLocal物件被回收,但是ThreadLocalMap還持有ThreadLocal的強參照,如果沒有手動刪除,ThreadLocal不會被回收,也會導致記憶體洩露。設定為弱參照後,參照的ThreadLocal物件被回收,由於ThreadLocalMap持有ThreadLocal的弱參照,即使沒有手動刪除,ThreadLocal也會被java GC回收。value在下一次ThreadLocalMap呼叫set、get、remove的時候會被清除。
參考文章:
https://www.cnblogs.com/tiancai/p/13141234.html?ivk_sa=1024320u
https://last2win.com/2020/09/05/java-threadlocal/
https://blog.csdn.net/u010445301/article/details/111322569
作者:京東零售 李澤陽
來源:京東雲開發者社群 轉載請註明來源