關于ThreadLocal使用時OOM的討論
之前介紹Spring bean線程安全的問題時候,討論到 ThreadLocal
類提供了線程局部變量,每個線程可以將一個值存在 ThreadLocal
對象中,其他線程無法訪問這些值。每個線程都有自己獨立的變量副本。
但如果使用不當,它可能會導致 內存泄漏(Memory Leak),最終引發(fā) (OOM)。根本原因在于 ThreadLocal 的存儲機制 和 垃圾回收(GC)行為。
1、數(shù)據(jù)結構
位于java.lang包下面。
1.1、內存存儲結構
ThreadLocal 的核心存儲依賴于:
ThreadLocalMap
(每個Thread
內部維護的一個類似WeakHashMap
的結構)Entry
(ThreadLocalMap
的存儲單元,key
是ThreadLocal
本身,value
是存儲的值)
如下圖所示:
定義時候,可參考如下:
ThreadLocal.ThreadLocalMap threadLocals; // 每個線程的 ThreadLocal 數(shù)據(jù)存儲在這里
ThreadLocalMap
的 Entry
是 弱引用(WeakReference) 的:
static class Entry extends WeakReference<ThreadLocal<?>> { Object value; // 存儲的值是強引用 Entry(ThreadLocal<?> k, Object v) { super(k); // key(ThreadLocal)是弱引用 value = v; // value 是強引用 } }
而對于key是弱引用,value是強引用。
2. 內存泄漏
如下圖所示:
ThreadLocal 的內存泄漏問題主要發(fā)生在 線程池環(huán)境(如 Tomcat、Spring 的異步任務等),因為線程會被復用,導致 ThreadLocalMap
長期存活。
2.1、引用回收
key
(ThreadLocal)是弱引用:
- 如果
ThreadLocal
對象沒有外部強引用(比如static
修飾),它會被 GC 回收,Entry
的key
變成null
。
value
是強引用:
- 即使
key
被回收,value
仍然被ThreadLocalMap
強引用,無法被 GC 回收。
2.2、value的強引用目的
1、如果是弱引用,調用get方法,返回為null,value
可能被提前回收,導致數(shù)據(jù)丟失。
2、設計目標是 讓每個線程可以安全地存儲自己的數(shù)據(jù),而不是讓數(shù)據(jù)隨時可能被回收。如果 value
是弱引用,就失去了存儲數(shù)據(jù)的可靠性。
2.3、線程長期存活
如果線程是線程池中的(如 Tomcat 的工作線程),線程不會銷毀,ThreadLocalMap
會一直存在。
如果 ThreadLocal
使用后沒有 remove()
,value
會一直占用內存,最終導致 內存泄漏。
示例如下:
public class UserContextHolder { private static ThreadLocal<User> userHolder = new ThreadLocal<>(); public static void set(User user) { userHolder.set(user); } public static User get() { return userHolder.get(); } // 忘記調用 remove()! }
問題:
- 每次 HTTP 請求結束后,Tomcat 線程不會銷毀,而是放回線程池。
- 如果
User
對象很大,多次請求后,ThreadLocalMap
會積累大量User
對象,最終 OOM。
小結
3、處理方案
先根據(jù)數(shù)據(jù)結構進行分析,如下圖所示:
3.1、remove
try { UserContextHolder.set(user); // ...業(yè)務邏輯 } finally { UserContextHolder.remove(); // 必須清理! }
最佳實踐:在 finally
塊中調用 remove()
,確保即使發(fā)生異常也能清理。
3.2、static修飾
private static final ThreadLocal<User> userHolder = new ThreadLocal<>();
原因:防止 ThreadLocal
被意外回收(弱引用失效)。
3.3、避免存儲大對象
如果 ThreadLocal
存儲的是大對象(如緩存、Session 數(shù)據(jù)),考慮改用其他方式(如 Redis)。
3.4、InheritableThreadLocal
InheritableThreadLocal
會傳遞給子線程,如果子線程不清理,同樣會導致內存泄漏。
小結
總結
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
java組件commons-fileupload實現(xiàn)文件上傳、下載、在線打開
這篇文章主要介紹了java組件commons-fileupload實現(xiàn)文件上傳、下載、在線打開,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-10-10ThreadLocal數(shù)據(jù)存儲結構原理解析
這篇文章主要為大家介紹了ThreadLocal數(shù)據(jù)存儲結構原理解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-10-10java 多線程Thread與runnable的區(qū)別
這篇文章主要介紹了java 多線程Thread與runnable的區(qū)別的相關資料,java線程有兩種方法繼承thread類與實現(xiàn)runnable接口,下面就提供實例幫助大家理解,需要的朋友可以參考下2017-08-08