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();
5)必须牢记的坑
坑 1:线程池复用导致“串号”
Tomcat 处理请求是线程池:线程会复用。
你这次请求 set 了 id=10,如果不 remove,下次这个线程处理别人的请求,可能还能 get 到 10 —— 严重安全问题。
✅ 结论:用完必须 remove(最好放 finally)
坑 2:内存泄漏风险(长生命周期线程)
ThreadLocal 的底层是 Thread 里维护一个 map。
如果你不断 set 很大的对象、又不 remove,在长时间运行的线程里会占用内存。
✅ 结论:ThreadLocal 里尽量存轻量数据(如 userId),并及时清理。
6)一句话把 ThreadLocal 和“全局变量”区分开
- 全局变量:所有线程共享 → 容易并发污染
- ThreadLocal:每个线程一份 → 只在当前线程可见