在多线程开发中,ThreadLocal 是一个非常实用的工具,它为每个线程提供独立的变量副本,从而避免线程间共享数据引发的并发问题。然而,如果使用不当,尤其是在使用线程池的场景下,ThreadLocal 也可能导致内存泄漏。理解这个问题,首先需要掌握 Java 中的四种引用类型。

1. Java 的四种引用类型(前置知识)

Java 中对象引用类型有四种:强引用、软引用、弱引用、虚引用

  1. 强引用(Strong Reference)

    • 默认引用类型,例如 String str = new String("Hello")

    • 对象只要被强引用持有,就不会被 GC 回收。(宁愿OOM也不回收

    • GC 不会因为内存不足而回收强引用对象。

    • 比喻:对象被“绑着走”,引用消失后对象才能被回收。

  2. 软引用(Soft Reference)

    • 通过 SoftReference 创建。

    • 对象只有在 JVM 内存不足时才会被回收,常用于缓存。

    • 比喻:对象像“可怜的小猫”,内存紧张时才会被收走。

  3. 弱引用(Weak Reference)

    • 通过 WeakReference 创建。

    • GC 一旦发现对象只被弱引用持有,就立即回收。(抗不住gc的毒打

    • ThreadLocalMap 中的 key 就是弱引用。

    • 比喻:对象像“纸片”,GC 来就扫掉。

  4. 虚引用(Phantom Reference)

    • 通过 PhantomReference 创建。

    • 永远不能通过 get() 获取对象,只能在对象被 GC 回收时收到通知

    • 主要用于对象回收前的清理工作。

    • 比喻:对象像“公告牌”,被 GC 收走前给你发通知,但你拿不到它。

2. ThreadLocal 的作用和典型使用场景

ThreadLocal 提供线程级别的数据存储,每个线程都有独立的副本(相当于捆绑,每个线程有一个自己的储物柜),避免线程安全问题。

主要作用:

  1. 线程内数据传递:在同一个线程执行过程中,ThreadLocal 的数据可以在线程方法链中传递,而无需通过方法参数传递

  2. 解决并发问题:为每个线程提供独立的数据副本,避免共享资源的竞争

典型使用场景:

  • 用户身份信息存储:请求拦截器中存储用户 ID、权限信息。

  • 线程安全对象:例如每线程独立的 SimpleDateFormat 实例。

  • 日志上下文存储:每条日志记录当前线程的用户信息。

  • TraceId 存储:分布式链路追踪中存储请求的 TraceId。

  • 数据库 Session 管理:ORM 框架中为每个线程管理独立的会话。


3. ThreadLocal 的实现原理

每个线程维护一个 ThreadLocalMap,本质上是一个线程内部的“仓库”。

ThreadLocalMap 结构:

  • Entry:每个 Entry 是一个 <ThreadLocal, value>

    • key:ThreadLocal 对象(弱引用)

    • value:存储的数据(强引用)

  • 数组存储:Entry 数组用 ThreadLocal 的 threadLocalHashCode 快速定位存储位置

ThreadLocal 的基本操作:

  • set():获取当前线程,拿到 ThreadLocalMap,把 this(ThreadLocal 对象)作为 key,value 存入 map

  • get():获取当前线程,拿到 ThreadLocalMap,通过 this 取出对应 value

public class ThreadLocal<T> {
    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    }
}

赋:图示底层数据结构展示(一个threadLocalMap对应就是一个Entry数组):

Thread
 └── ThreadLocalMap
        └── Entry[]
             ├── Entry[...] → (tl1 弱引用, "A")
             ├── Entry[...] → (tl2 弱引用, "B")
             └── Entry[...] → (tl3 弱引用, "C")

4. ThreadLocal 内存泄漏问题

内存泄漏定义: 对象无法被回收,长期占用内存。

ThreadLocal 内存泄漏的原因:

  1. ThreadLocalMap 中的 key 是弱引用,GC 会回收 ThreadLocal 对象

  2. value 是强引用,即使 key 被回收,value 仍然存在

  3. 线程池中线程长期存在,ThreadLocalMap 随线程存活,value 不被回收

示意:

解析:1到2是强引用,随着1的消失,2会被gc清理掉(因为1一旦消失就没有指向2的强引用了),一旦2消失和他捆绑的3就一起消失了(因为捆绑在一起),此时相当于entry中key没了,value还在,也就存在了内存泄露。(因为此时thred对象和value相当于是共生的关系,如果在线程池的背景下,相当于线程不断的被复用,更加容易造成oom了

解决方案:
使用完 ThreadLocal 后,必须调用 remove() 清理 value,尤其是线程池环境下。

try {
    threadLocal.set(value);
    // 使用线程局部变量
} finally {
    threadLocal.remove(); // 避免内存泄漏
}

5. 总结

  • ThreadLocal 为每个线程提供独立变量副本,简化并发安全设计。

  • 使用时必须注意线程池环境下的内存泄漏问题,原因是 ThreadLocalMap 中 value 的强引用残留。

  • 掌握 强/软/弱/虚引用 的区别,可以更好理解 ThreadLocal 的实现和潜在风险。

  • 正确做法是在 finally 中清理 ThreadLocal,以保证数据及时释放。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐