Appearance
ThreadLocal 为什么容易出问题:线程复用、内存泄漏和上下文污染
ThreadLocal 很好用,因为它让“当前线程自己的上下文数据”拿起来特别顺手。
但线上环境里,它也特别容易埋雷,尤其一旦和线程池放在一起。
先说结论
ThreadLocal 适合做线程内上下文传递,但一定要牢记:
- 线程池里的线程会复用
- 用完不清理,就可能污染后续请求
- 存大对象或长生命周期对象,风险会更高
一、ThreadLocal 解决了什么
它最常见的用途是:
- 保存当前请求上下文
- 保存用户信息
- 保存链路追踪 ID
- 避免一层层传参
所以它的价值主要在:
- 线程内隔离
二、为什么它一到线上就容易出问题
因为线上服务通常不是“开一个线程处理完就销毁”,而是:
- 使用线程池复用线程
这时如果某个请求把数据塞进 ThreadLocal 却没清理,后面复用同一个线程的新请求就可能读到旧数据。
这会带来两类问题:
1. 上下文污染
比如:
- 用户 A 的上下文被用户 B 读到
- 老请求残留的 traceId 影响新请求日志
2. 内存问题
如果 ThreadLocal 里挂的是大对象,线程又长期不销毁,就可能导致对象一直挂着不释放。
三、线程池场景为什么特别危险
很多业务会把请求上下文透传到异步线程。
但要注意:
- 子线程不是天然继承主线程 ThreadLocal 的
- 就算人为复制过去,也要负责清理
否则很容易变成:
- 线程复用
- 数据残留
- 排查困难
四、最重要的使用原则
1. 用完一定 remove
这是最关键的一条。
java
try {
contextHolder.set(userInfo);
// do something
} finally {
contextHolder.remove();
}2. 不要往里面塞过大的对象
尤其不要无上限地往里挂:
- 大集合
- 大缓存对象
- 大文件内容
3. 不要把它当跨线程共享工具
它解决的是线程内上下文,不是线程间通信。
五、哪些场景适合用 ThreadLocal
适合
- 请求上下文
- 当前登录用户
- traceId
- 临时线程级状态
不适合
- 跨线程共享数据
- 长期缓存对象
- 不方便统一清理的状态
一句话总结
ThreadLocal 的价值在于“线程内方便”,它的风险也恰恰来自“线程会被长期复用”。
所以真正安全的使用方式不是会不会 set,而是有没有在合适的边界里及时 remove。