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()。

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