ThreadLocal

ThreadLocal

一句话总结

ThreadLocal 用于为每个线程 保存一份互相隔离 的变量副本:同一个 ThreadLocal 在不同线程里 get() 到的是不同值。其底层是每个 Thread 维护一个 ThreadLocalMap,以 ThreadLocal 实例作为 key(弱引用),以实际数据作为 value。在线程池场景必须 remove(),否则可能发生"串数据"和内存泄漏。


1. 适用场景(什么时候用)

适合"线程维度"的上下文数据:

  • 请求上下文traceId / requestId / 当前登录用户信息(仅限当前线程内)
  • 资源句柄绑定到线程:数据库连接、事务上下文、会话对象(常见于框架内部实现)
  • 跨方法传参:避免层层方法增加参数(但要克制,防止隐式依赖)
  • 非线程安全对象的线程隔离 :如 SimpleDateFormat(更推荐 DateTimeFormatter

不适合:

  • 需要在线程之间共享/汇总的数据(ThreadLocal 天生隔离)
  • 依赖线程池但又不清理的上下文(容易污染复用线程)

2. 基本用法(务必带清理)

2.1 最小示例

java 复制代码
public class ThreadLocalDemo {
    private static final ThreadLocal<Integer> TL = ThreadLocal.withInitial(() -> 0);

    public static void main(String[] args) {
        TL.set(100);
        try {
            System.out.println(TL.get());
        } finally {
            // 线程池/长生命周期线程中必须清理
            TL.remove();
        }
    }
}

2.2 Web 请求:Filter / Interceptor 中 set 与 finally 清理

java 复制代码
public final class RequestContext {
    private RequestContext() {}

    private static final ThreadLocal<String> TRACE_ID = new ThreadLocal<>();

    public static void setTraceId(String id) { TRACE_ID.set(id); }
    public static String getTraceId() { return TRACE_ID.get(); }
    public static void clear() { TRACE_ID.remove(); }
}

// 伪代码:Filter / Interceptor
try {
    RequestContext.setTraceId(traceId);
    chain.doFilter(req, resp);
} finally {
    RequestContext.clear();
}

3. 实现原理(面试核心)

3.1 核心结构:Thread 持有 ThreadLocalMap

  • 不是 ThreadLocal 自己存值
  • 而是 Thread 内部有一个 ThreadLocalMap,以 ThreadLocal 为 key 存储 value

概念示意:

复制代码
Thread
 └── ThreadLocalMap
      ├── Entry(key=WeakReference<ThreadLocal>, value=Object)
      ├── Entry(...)
      └── ...

3.2 set / get / remove 的关键流程

  • set(value)

    1. 取当前线程 Thread.currentThread()
    2. 拿到该线程的 ThreadLocalMap(没有则创建)
    3. 以"当前 ThreadLocal 实例"为 key 写入 value
  • get()

    1. 取当前线程的 ThreadLocalMap
    2. 用"当前 ThreadLocal 实例"为 key 查找
    3. 若没有命中:调用 initialValue() / withInitial(...) 生成并写入,再返回
  • remove()

    • 删除当前线程 ThreadLocalMap 中对应 key 的 Entry

4. 为什么会内存泄漏?(以及为什么 key 用弱引用仍可能泄漏)

4.1 关键点

ThreadLocalMap.Entry 的 key 是 WeakReference<ThreadLocal<?>>

  • ThreadLocal 实例没有强引用 后,key 可能被 GC 回收,变成 null
  • value 是强引用 ,仍然挂在当前线程的 ThreadLocalMap
  • 如果线程是长生命周期(尤其线程池工作线程),value 可能长期无法释放

这类 Entry 常被称为 stale entry(陈旧条目)key == nullvalue != null

4.2 为什么"看起来弱引用了"还要 remove?

因为:

  • GC 只能回收 key(ThreadLocal 对象),回收不了 value
  • ThreadLocalMap 会在 set/get/remove 时顺带做一些清理,但并不可靠/不及时
  • 线程池线程复用导致 value 长时间存活,甚至"下一个任务读到上一个任务的上下文"(串数据)

4.3 结论

  • 业务代码原则:set 后必须 remove(try/finally)
  • 特别是在 线程池、Web 容器线程、RPC 框架线程等长生命周期线程中

5. 与 synchronized / Lock 的对比

维度 ThreadLocal synchronized / Lock
思路 空间换时间:每线程一份副本 时间换空间:共享数据加锁
数据可见性 线程隔离,不共享 多线程共享同一份数据
性能 无锁访问,通常更快 有锁竞争与上下文切换开销
适用场景 线程上下文、线程绑定资源 共享资源并发读写
风险 线程池污染、内存泄漏(需清理) 死锁、锁竞争、性能抖动

6. InheritableThreadLocal 与线程池"上下文传递"

6.1 InheritableThreadLocal

  • 作用:子线程创建时,从父线程拷贝一份初始值
  • 局限:
    • 只在"创建子线程"那一刻拷贝
    • 在线程池里线程通常提前创建并复用,所以几乎不生效,甚至造成误解

6.2 线程池中的上下文传递(常见方案)

  • 装饰 Runnable/Callable:提交任务前捕获上下文,执行后清理
  • 使用成熟方案(如阿里 TTL:TransmittableThreadLocal
    • 解决"线程池复用"导致的上下文无法自动传递的问题
    • 仍然要理解其原理与清理策略,避免滥用

7. 最佳实践(直接背)

  1. 必须 try/finally 调用 remove()(尤其线程池)。
  2. ThreadLocal 建议定义为 private static final(同一语义复用同一个 ThreadLocal 实例)。
  3. value 尽量小、生命周期短,避免缓存大对象。
  4. 不要把 ThreadLocal 当"全局变量/隐式参数"滥用:会让代码依赖关系变隐蔽,难测试。
  5. 框架层(Filter/Interceptor/AOP)统一管理 set/clear,业务代码尽量只读。
  6. 出现"用户串号/traceId 混乱/数据偶现"优先排查 ThreadLocal 是否在线程池中未清理。

8. 高频面试问答

Q1:ThreadLocal 是怎么做到线程隔离的?

每个 Thread 内部维护一个 ThreadLocalMap,同一个 ThreadLocal 在不同线程里对应不同 Entry/value,因此天然隔离。

Q2:ThreadLocal 的 key 为什么用弱引用?

为了避免 ThreadLocal 对象本身无法回收:当外部不再持有 ThreadLocal 的强引用时,弱引用 key 可被 GC。

Q3:key 是弱引用为什么还会内存泄漏?

因为 value 仍是强引用,且挂在长生命周期线程的 ThreadLocalMap 上;key 被回收后会留下 stale entry,若没有及时清理(remove 或 map 自清理触发),value 仍可能长期占用内存。

Q4:线程池里 ThreadLocal 的典型问题是什么?

  • 上下文污染/串数据:线程复用导致上一次任务的 value 被下一次任务读到
  • 内存泄漏:value 长期挂在线程池工作线程上

解决:任务执行完 finally remove,或使用上下文传递封装/TTL 并配合清理。

Q5:ThreadLocalMap 是怎么处理哈希冲突的?

采用开放寻址(线性探测)方式:发生冲突就向后寻找空槽位;并在访问过程中机会性清理 stale entry。

相关推荐
消失的旧时光-19431 小时前
C++ 多线程与并发系统取向(二)—— 资源保护:std::mutex 与 RAII(类比 Java synchronized)
java·开发语言·c++·并发
学习是生活的调味剂2 小时前
spring bean循环依赖问题分析
java·后端·spring
Coder_Boy_3 小时前
Java(Spring AI)传统项目智能化改造——商业化真实案例(含完整核心代码+落地指南)
java·人工智能·spring boot·spring·微服务
五阿哥永琪3 小时前
1. 为什么java不能用is开头来做布尔值的参数名,会出现反序列化异常。
java·开发语言
chilavert3184 小时前
技术演进中的开发沉思-371:final 关键字(中)
java·前端·数据库
海边的Kurisu5 小时前
Mybatis-Plus | 只做增强不做改变——为简化开发而生
java·开发语言·mybatis
识君啊5 小时前
Java 二叉树从入门到精通-遍历与递归详解
java·算法·leetcode·二叉树·深度优先·广度优先
daidaidaiyu5 小时前
一文学习 Spring AOP 源码全过程
java·spring