你们用 ThreadLocal 传用户信息或 traceId 吗?线程池复用时怎么避免数据串号和内存泄漏?
这个问题我会先说项目结论:ThreadLocal 为同一变量提供每线程独立副本,数据存在线程对象内部的 ThreadLocalMap。Map 的键是 ThreadLocal 弱引用,值仍是强引用;键被回收后,如果线程长期存活且没有触发清理,值可能继续滞留。
我会先交代项目背景和选型结论。理解线程本地变量、弱引用键、线程池复用与上下文清理。ThreadLocal 为同一变量提供每线程独立副本,数据存在线程对象内部的 ThreadLocalMap。Map 的键是 ThreadLocal 弱引用,值仍是强引用;键被回收后,如果线程长期存活且没有触发清理,值可能继续滞留。 在线程池中线程反复复用,不清理还会把上一个请求的用户、租户或链路信息泄露给下一个任务。
具体落地时,我会沿着实际调用链来讲。沿着「线程访问 ThreadLocal → 按弱引用 Key 查找槽位 → 初始化并保存线程上下文 → 业务链路读取 → finally remove 清理」观察输入、状态与输出,这些阶段都可以从日志、指标或源码里验证。 线程访问 ThreadLocal 每个 Thread 持有自己的 ThreadLocalMap,数据跟随线程而不是 ThreadLocal 对象本身。 ThreadLocal$ThreadLocalMap:弱 Key、强 Value 与 stale entry。 ThreadLocal#remove:清除当前线程槽位。 我会先确认请求实际走到了哪条路径,再用运行数据验证,不会只看类名或配置猜测。
参数和容量不能靠默认值,我会结合业务量来定。单线程池依次处理两个租户请求,第一请求故意异常;断言第二请求读不到旧上下文,并用堆转储检查 Value 保留。
效果要用数据证明,线上问题也要能闭环。固定输入和基线 先在没有故障注入的环境执行上述配置,固定数据规模、并发度、运行时版本和预热时间。以「请求后残留」为主基线,记录值应满足「目标 0」;同时保存 线程本地 Value 大小、请求后残留上下文,使后续变化能够回到同一时间轴比较。 请求 A 设置租户后异常返回,未执行 remove;同一工作线程处理请求 B 时读到 A 的租户,造成越权查询。把设置与清理封装为可关闭作用域,并在入口过滤器 finally 清理后才消除污染。 线程池请求结束未 remove:线程本地 Value 大小:入口统一 scope/finally。
最后我会主动说明这个方案不适合什么场景。方案:更适合的场景:主要收益:代价与边界。 ThreadLocal:同步调用链中的请求级隐式上下文:接入简单、读取快:线程池污染和异步传播困难。 显式参数:核心业务数据与跨线程调用:依赖清晰、易测试:调用链参数较多。 ScopedValue/框架上下文:结构化并发或已有上下文传播设施:作用域明确、自动恢复:受 JDK 或框架版本约束。