初识ThreadLocal

9 阅读2分钟

ThreadLocal 可以把它理解成: “每个线程自己带的小抽屉” 。同一个变量名,在不同线程里看到的值互不影响,所以非常适合在 同一个请求(同一个线程) 的多个层之间共享数据。


1)ThreadLocal 是什么?

  • ThreadLocal 不是线程,而是一个工具类。
  • 它给每个线程提供一份独立的变量副本(线程隔离)。
  • 所以:A 线程 set(1),B 线程 get() 看不到 A 的值。

总结:

  • 是线程的局部变量
  • 线程隔离,不相互干扰

2)它解决什么问题?(为什么要用)

因为 Web 项目里经常会有这种需求:

登录校验(Filter/Interceptor)里解析 token 得到 userId
然后 Controller/Service/AOP 都需要用到这个 userId
但你又不想每一层方法都加一个 userId 参数一路传下去(太丑)

ThreadLocal 就能让你在同一次请求里“随取随用”。


3)典型应用场景

同一个线程 / 同一个请求中共享数据

  • Filter/Interceptor:解析 JWT → ThreadLocal.set(userId)
  • AOP 记录日志:ThreadLocal.get() 拿当前操作人
  • 请求结束:ThreadLocal.remove() 清理

还常见于:

  • 日志链路跟踪(MDC)
  • 多数据源切换(当前线程选哪个库)
  • 事务上下文(框架内部也类似思路)

4)怎么用?(最小可用示例)

① 写一个 CurrentHolder(工具类)

public class CurrentHolder {
    private static final ThreadLocal<Integer> TL = new ThreadLocal<>();

    public static void set(Integer id) { TL.set(id); }
    public static Integer get() { return TL.get(); }
    public static void remove() { TL.remove(); }
}

② 在 Filter / Interceptor 里设置 & 清理

关键点:一定要 remove! (尤其线程池环境)

try {
    Integer empId = ... // 解析JWT得到
    CurrentHolder.set(empId);

    chain.doFilter(request, response); // 放行
} finally {
    CurrentHolder.remove(); // 必须清理,避免串号/内存泄漏风险
}

③ 在 AOP 里直接取

Integer empId = CurrentHolder.get();

image.png


5)必须牢记的坑

坑 1:线程池复用导致“串号”

Tomcat 处理请求是线程池:线程会复用。
你这次请求 set 了 id=10,如果不 remove,下次这个线程处理别人的请求,可能还能 get 到 10 —— 严重安全问题

✅ 结论:用完必须 remove(最好放 finally)


坑 2:内存泄漏风险(长生命周期线程)

ThreadLocal 的底层是 Thread 里维护一个 map。
如果你不断 set 很大的对象、又不 remove,在长时间运行的线程里会占用内存。

✅ 结论:ThreadLocal 里尽量存轻量数据(如 userId),并及时清理。


6)一句话把 ThreadLocal 和“全局变量”区分开

  • 全局变量:所有线程共享 → 容易并发污染
  • ThreadLocal:每个线程一份 → 只在当前线程可见