Skip to content

ThreadLocal详解

ThreadLocal 用来保存线程私有变量。不同线程访问同一个 ThreadLocal 对象时,拿到的是各自线程内部保存的值,互不影响。

如果要完整理解 set/get/remove 流程、ThreadLocalMap、弱引用 key、强引用 value、线程池串数据、InheritableThreadLocal、异步上下文传递和线上排查,建议继续看:ThreadLocal全过程原理

常见场景:

  • 保存当前登录用户。
  • 保存 traceId。
  • 保存数据库连接或事务上下文。
  • 保存日期格式化对象。

基本原理

ThreadLocal 的数据并不是存在 ThreadLocal 对象本身,而是存在当前线程的 ThreadLocalMap 中。

mermaid
flowchart TD
    A[Thread] --> B[ThreadLocalMap]
    B --> C[Entry: ThreadLocal A -> valueA]
    B --> D[Entry: ThreadLocal B -> valueB]

可以理解为:每个线程都有一个自己的 Map,ThreadLocal 对象只是这个 Map 的 key。

更精确地说:是每个 Thread 对象内部有一个 ThreadLocalMap,不是每个 ThreadLocal 自己有一个 Map。

mermaid
flowchart TD
    A["Thread-1"] --> B["ThreadLocalMap-1"]
    C["Thread-2"] --> D["ThreadLocalMap-2"]
    E["同一个 ThreadLocal 对象"] --> B
    E --> D
    B --> F["Entry: threadLocal -> valueA"]
    D --> G["Entry: threadLocal -> valueB"]

所以同一个 ThreadLocal 在不同线程里能拿到不同值,是因为它作为 key,分别存进了不同线程自己的 ThreadLocalMap。

set 和 get 流程

mermaid
flowchart TD
    A[ThreadLocal.set(value)] --> B[获取当前线程]
    B --> C[获取当前线程的 ThreadLocalMap]
    C --> D{Map 是否存在?}
    D -->|否| E[创建 ThreadLocalMap]
    D -->|是| F[以 ThreadLocal 为 key 保存 value]
    E --> F
    G[ThreadLocal.get] --> H[获取当前线程 Map]
    H --> I[用当前 ThreadLocal 查找 value]

为什么会内存泄漏

ThreadLocalMap 的 key 是弱引用,value 是强引用。如果 ThreadLocal 对象被回收,key 会变成 null,但 value 还在 ThreadLocalMap 中。

在线程池场景下,线程会被长期复用,如果不清理 value,就可能造成内存泄漏。

mermaid
flowchart TD
    A[线程池线程长期存活] --> B[ThreadLocal key 被回收]
    B --> C[Entry key 变成 null]
    C --> D[value 仍被 ThreadLocalMap 引用]
    D --> E[内存无法释放]

这里最容易误解的是:key 是弱引用,不代表 value 会自动释放。

ThreadLocalMap 的 Entry 可以简化理解为:

text
Entry(WeakReference<ThreadLocal<?>> key, Object value)

当外部不再引用 ThreadLocal 对象时,GC 可以回收 key 指向的 ThreadLocal,于是 Entry 的 key 变成 null。但 Entry 自己还在当前线程的 ThreadLocalMap 中,value 仍然被 Entry 强引用。只要线程不结束,value 就可能一直活着。

普通临时线程问题不明显,因为线程执行结束后,Thread 对象和 ThreadLocalMap 会一起回收。线程池线程会长期存在,所以问题更常见。

key 为什么设计成弱引用

如果 key 是强引用,那么只要线程还活着,ThreadLocalMap 就会一直强引用 ThreadLocal 对象。即使业务代码已经不再持有这个 ThreadLocal,它也无法被 GC。

弱引用的设计至少让 ThreadLocal 对象本身有机会被回收,避免 key 永远占着不放。但它不能自动解决 value 的强引用问题,所以最终仍然要求开发者使用后 remove()

mermaid
flowchart TD
    A["业务代码不再引用 ThreadLocal"] --> B{"key 是强引用吗"}
    B -- "是" --> C["ThreadLocal 仍被 ThreadLocalMap 持有<br/>无法回收"]
    B -- "否,弱引用" --> D["ThreadLocal 可以被 GC"]
    D --> E["Entry key 变 null"]
    E --> F["value 仍需 remove 或后续清理"]

为什么会串数据

线程池会复用线程。如果上一个请求设置了 ThreadLocal 但没有 remove,下一个请求刚好复用同一个线程,就可能读到上一个请求的用户、租户或 traceId。

mermaid
flowchart TD
    A["请求1 在线程T执行"] --> B["USER_CONTEXT.set(userA)"]
    B --> C["业务结束但没有 remove"]
    C --> D["线程T回到线程池"]
    D --> E["请求2 复用线程T"]
    E --> F["USER_CONTEXT.get() 读到 userA"]

所以 ThreadLocal 的风险不只是内存泄漏,还包括身份串用、租户串用、日志 traceId 串用。这类问题在线上非常隐蔽,因为它取决于线程复用时机。

代码 Demo:正确使用方式

java
try {
    USER_CONTEXT.set(user);
    // 执行业务逻辑
} finally {
    USER_CONTEXT.remove();
}

关键点是 finally 中调用 remove(),确保无论业务成功还是异常都能清理。

更完整的上下文 Demo:

java
public final class TraceContext {
    private static final ThreadLocal<String> TRACE_ID = new ThreadLocal<>();

    private TraceContext() {
    }

    public static void set(String traceId) {
        TRACE_ID.set(traceId);
    }

    public static String get() {
        return TRACE_ID.get();
    }

    public static void clear() {
        TRACE_ID.remove();
    }
}

使用时必须成对出现:

java
try {
    TraceContext.set(request.getHeader("X-Trace-Id"));
    collectService.collect();
} finally {
    TraceContext.clear();
}

如果在过滤器、拦截器里设置 ThreadLocal,也要在请求返回阶段清理。

InheritableThreadLocal

InheritableThreadLocal 可以让子线程继承父线程的变量。但在线程池中要谨慎使用,因为线程不是每次都新建,可能继承不到预期值,或者复用旧值。

如果需要在线程池中传递上下文,建议使用明确的上下文传递方案,不要完全依赖 InheritableThreadLocal。

常见问题

ThreadLocal 能解决线程安全吗

它解决的是变量隔离,不是共享变量加锁。如果多个线程操作同一个共享对象,ThreadLocal 本身不能保证对象内部线程安全。

为什么线程池里更容易出问题

普通线程执行结束后,线程对象会被回收,ThreadLocalMap 也会随之释放。线程池线程会长期存在,如果不 remove,旧数据可能污染下一次任务。

总结

ThreadLocal 适合保存“当前线程上下文”,但必须遵守两个原则:

  1. 不要保存过大的对象。
  2. 使用完成后必须 remove()

并发基础可以回到 多线程总览,锁相关内容可以看 synchronizedAQS