关于ThreadLocal使用时OOM的讨论

 更新时间:2025年06月29日 13:40:01   作者:找不到、了  
这篇文章主要介绍了关于ThreadLocal使用时OOM的讨论,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

之前介绍Spring bean线程安全的问题时候,讨论到 ThreadLocal 类提供了线程局部变量,每个线程可以将一个值存在 ThreadLocal 对象中,其他线程无法访问这些值。每个线程都有自己独立的变量副本。

但如果使用不当,它可能会导致 内存泄漏(Memory Leak),最终引发 (OOM)。根本原因在于 ThreadLocal 的存储机制 和 垃圾回收(GC)行为

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是强引用。

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

小结

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • java组件commons-fileupload实现文件上传、下载、在线打开

    java组件commons-fileupload实现文件上传、下载、在线打开

    这篇文章主要介绍了java组件commons-fileupload实现文件上传、下载、在线打开,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-10-10
  • Java之SpringCloud nocos注册中心讲解

    Java之SpringCloud nocos注册中心讲解

    这篇文章主要介绍了Java之SpringCloud nocos注册中心讲解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下
    2021-08-08
  • 详解领域驱动设计之事件驱动与CQRS

    详解领域驱动设计之事件驱动与CQRS

    这篇文章分析了如何应用事件来分离软件核心复杂度。探究CQRS为什么广泛应用于DDD项目中,以及如何落地实现CQRS框架。当然我们也要警惕一些失败的教训,利弊分析以后再去抉择正确的应对之道
    2021-06-06
  • idea导入项目爆红问题记录以及解决

    idea导入项目爆红问题记录以及解决

    这篇文章主要介绍了idea导入项目爆红问题记录以及解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-07-07
  • SpringBoot项目jar和war打包部署方式详解

    SpringBoot项目jar和war打包部署方式详解

    这篇文章主要为大家介绍了SpringBoot项目jar和war打包部署方式详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-09-09
  • ThreadLocal数据存储结构原理解析

    ThreadLocal数据存储结构原理解析

    这篇文章主要为大家介绍了ThreadLocal数据存储结构原理解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-10-10
  • Java中泛型使用实例详解

    Java中泛型使用实例详解

    这篇文章主要介绍了Java中泛型使用实例详解的相关资料,需要的朋友可以参考下
    2017-05-05
  • java高级应用:线程池的全面讲解(干货)

    java高级应用:线程池的全面讲解(干货)

    这篇文章主要介绍了java高级应用:线程池的全面讲解(干货),具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-02-02
  • 基于zipoutputStream的简单使用

    基于zipoutputStream的简单使用

    这篇文章主要介绍了基于zipoutputStream的简单使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-12-12
  • java 多线程Thread与runnable的区别

    java 多线程Thread与runnable的区别

    这篇文章主要介绍了java 多线程Thread与runnable的区别的相关资料,java线程有两种方法继承thread类与实现runnable接口,下面就提供实例帮助大家理解,需要的朋友可以参考下
    2017-08-08

最新评论