threadlocal.set() 为当前线程创建独立副本,非全局共享;每个线程通过 threadlocalmap 存储以自身为 key 的键值对,需 static final 声明、及时 remove() 防内存泄漏,子线程默认不继承,大对象或资源须谨慎存储。

ThreadLocal.set() 存的是当前线程的副本,不是全局变量
很多人第一次用 ThreadLocal 时误以为 set() 是“设置一个共享值”,结果在多线程里发现值互相覆盖或取不到——其实根本没覆盖,只是每个线程看到的都是自己那份副本。调用 set(value) 时,JVM 实际往当前线程对象(Thread)内部的 threadLocals 字段(类型为 ThreadLocalMap)里塞了一个键值对:this(即当前 ThreadLocal 实例)为 key,value 为值。
所以你不需要加锁、也不用担心并发修改,只要确保:
- ThreadLocal 变量声明为 static final(否则不同实例无法复用,还可能引发内存泄漏)
- 每次 set() 前不依赖其他线程的状态
- 不在子线程里期望拿到父线程存的值(默认不继承)
必须显式调用 remove(),尤其在线程池场景下
这是生产环境最常踩的坑。Tomcat、Spring Boot 默认用线程池处理 HTTP 请求,一个线程会反复处理成百上千个请求。如果只 set() 不 remove(),上一个请求存的上下文(比如用户 ID、trace ID、大对象)就会一直留在该线程的 ThreadLocalMap 中,越积越多,最终触发内存泄漏。
典型错误模式:
- 在 Filter 或 Interceptor 里 set(),但没配对应的 finally { remove() }
- get() 后直接返回,忘了清理
- 使用了 withInitial(),误以为初始化后就自动管理生命周期
正确姿势是:
- 在请求入口(如 Servlet Filter 的 doFilter())中 set()
- 在 finally 块中强制 remove()
- 如果上下文对象较大,建议在 remove() 前先置 null 或清空内部引用
子线程拿不到父线程的 ThreadLocal 值,要用 InheritableThreadLocal
普通 ThreadLocal 的值不会自动传递给子线程。比如你在主线程里 set("req-123"),然后起一个新 Thread 或提交到 ExecutorService,子线程调用 get() 得到的是 null(或 initialValue() 返回值)。
解决办法只有两个:
- 改用 InheritableThreadLocal(继承自 ThreadLocal),它会在子线程创建时把父线程对应 key 的 value 拷贝一份过去
- 手动在创建子线程前 get() 出来,构造时传进去,再在子线程内 set()
注意:InheritableThreadLocal 的拷贝只发生在子线程**构造时**,后续父线程修改不影响子线程;且线程池中的“子线程”其实是复用的,不会触发继承逻辑——所以它只适用于 new Thread() 这种一次性线程。
避免存储大对象或未关闭资源,ThreadLocalMap 的 key 是弱引用但 value 是强引用
ThreadLocalMap.Entry 继承自 WeakReference<threadlocal></threadlocal>,这意味着:当外部不再持有 ThreadLocal 实例时,key 可被 GC 回收,但 value 仍被 Entry 强引用着。如果 value 是一个大对象(如 Map、Connection、完整用户上下文),而你又忘了 remove(),它就会一直卡在 ThreadLocalMap 里,直到线程结束。
真实案例中,有人存了一个含 50 个字段的 UserContext 对象,线程池有 200 个线程,7 天后光这部分就占了 1.2GB 堆内存。排查时发现 ThreadLocalMap 里大量 Entry 的 key 为 null,value 却非空——这就是典型的“key 被回收,value 滞留”泄漏模式。
因此:
- 尽量用轻量级数据(如 String、Long、UUID)
- 如果必须存对象,确保它不持有其他长生命周期引用
- 避免在 ThreadLocal 里存流、连接、缓存等需显式关闭的资源










