开发者

关于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 的结构)
    • EntryThreadLocalMap 的存储单元,key 是 ThreadLocal 本身,value 是存储的值)

    如下图所示:

    关于ThreadLocal使用时OOM的讨论

    定义时候,可参考如下:

    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使用时OOM的讨论

            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&nbspToeuvWRp;是弱引用,就失去了存储数据的可靠性。

    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。

    小结

    关于ThreadLocal使用时OOM的讨论

    3、处理方案

    先根据数据结构进行分析,如下图所示:

    关于ThreadLocal使用时OOM的讨论

    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果子线程不清理,同样会导致内存泄漏。

    小结

    关于ThreadLocal使用时OOM的讨论

    总结

    以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。

    0

    上一篇:

    下一篇:

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    最新开发

    开发排行榜