就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)行为

关于更多bean线程安全相关的可参考:关于Spring的bean线程安全讨论_怎样保证spring注入的bean线程安全-CSDN博客


1、数据结构

位于java.lang包下面。

1.1、内存存储结构

ThreadLocal 的核心存储依赖于:

  • ThreadLocalMap(每个 Thread 内部维护的一个类似 WeakHashMap 的结构)

  • EntryThreadLocalMap 的存储单元,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是弱引用,value是强引用。

关于强引用和弱引用回收,可参考:对Java 资源管理和引用体系的介绍-CSDN博客

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

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、处理方案

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

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、InheritableThreadLocal

InheritableThreadLocal 会传递给子线程,如果子线程不清理,同样会导致内存泄漏。

小结

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值