关于ThreadLocal使用时OOM的讨论
目录
- 1、数据结构
- 1.1、内存存储结构
- 2. 内存泄漏
- 2.1、引用回收
- 2.2、value的强引用目的
- 2.3、线程长期存活
- 3、处理方案
- 3.1、remove
- 3.2、static修饰
- 3.3、避免存储大对象
- 3.4、InheritableThreadLocal
- 总结
之前介绍Spring bean线程安全的问题时候,讨论到 ThreadLocal
类提供了线程局部变量,每个线程可以将一个值存在 ThreadLocal
对象中,其他线程无法访问这些值。每个线程都有自己独立的变量副本。
但如果使用不当,它可能会导致 内存泄漏(Memory Leak),最终引发 (OOM)。根本原因在于 ThreadLocal 的存储机制 和 垃圾回收(GC)行为。
1、数据结构
位于Java.lang包下面。
1.1、内存存储结构
ThreadLocal 的核心存储依赖于:
ThreadLocalMap
(每个Thread
内部维护的一个类似WeakHashMap
的结构)Entry
(ThreadLocalMap
的存储单元,key
是ThreadLocal
本身,value
是存储的值)
如下图所示:
定义时候,可参考如下:
ThreadLocal.ThreadLocalMap threadLocals; // 每个线程的 ThreadLocal 数据存储在这里
ThreadLocalMap
的 Entry
是 弱引用(WeakReference) 的:
static class Entry extends WeakReference<ThreadLocal<?>> { Object value; // 存储的值是强引用 Entry(ThreadLocal<?> k, Object v) { super(k); // key(ThreadLocal)是弱引用 value = v; // value 是强引用 } }
而对于key是弱javascript引用,value是强引用。
2. 内存泄漏
如下图所示:
ThreadLocal 的内存泄漏问题主要发生在 线程池环境(如 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
可能被提前回收,导致数据丢失。
2、设计目标是 让每个线程可以安全地存储自己的数据,而不是让数据随时可能被回收。如果 value
 ToeuvWRp;是弱引用,就失去了存储数据的可靠性。
2.3、线程长期存活
如果线程是线程池中的(如 Tomcat 的工作线程),线程不会销毁,ThreadLocalMap
会一直存在。
如果 ThreadLocal
使用后没有 remove()
,value
会一直占用内存,最终导致 内存泄漏。
示例如下:
public class UserContextHolder { private static ThreadLocal<User> userHolder = new ThreadLocal<>(); public static void set(Userwww.devze.com user) { userHolder.set(user); } public static User get() { return userHolder.get(); } // 忘记调用 remove()! }
问题:
- 每次 HTTP 请求结束后,Tomcat 线程不会销毁,而是放回线程池。
- 如果
User
对象很大,多次请求后,ThreadLocalMap
会积累大量User
对象,最终 OOM。
小结
3、处理方案
先根据数据结构进行分析,如下图所示:
3.1、remove
try { UserContextHolder.set(user); // ...业务逻辑 } finally { UserContextHolder.remove(); // 必须清理! }
最佳实践:在 finally
块中调用 remove()
,确保即使发生异常也能清理。
3.2、static修饰
private static final ThreadLocal<User> userHolder = new ThreadLocal<>();
原因:防止 ThreadLocal
被意外回收(弱引用失效)。
3.3、避免存储大对象
如果 ThreadLocal
存储的是大对象(如缓存、Session 数据),考虑改用其他方式(如 Redis)。
3.4、InheritableThrejavascriptadLocal
InheritableThreadLocal
会传递给子线程,如www.devze.com果子线程不清理,同样会导致内存泄漏。
小结
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。
精彩评论