ThreadLocal:线程私有副本
更新时间:2026-09-20。ThreadLocal 是避免共享的手段,和 synchronized/volatile 那种「治理共享」的思路相反;它也是 OOM 排查中的常见泄漏点,配合 OOM 排查 阅读。
一、解决什么问题
static final ThreadLocal<SimpleDateFormat> DF =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
String s = DF.get().format(new Date()); // 每线程各用各的实例SimpleDateFormat 非线程安全,若多个线程共用一个实例会算错或抛异常。ThreadLocal 给每个线程一份独立副本,互不干扰,无需加锁。典型场景:非线程安全工具类、用户/请求上下文、traceId、连接在「同一线程执行链」上的透传。
关键心智:ThreadLocal 不是「多个线程共享一个值」,而是「同一个逻辑变量,每线程各存一份」。
二、底层结构(容易记反)
不是 ThreadLocal 内部有个 Map 存所有线程,而是:
- 每个
Thread对象里有一个ThreadLocal.ThreadLocalMap; - Map 的 key 是 ThreadLocal 对象(弱引用 WeakReference),value 是你 set 的对象(强引用);
tl.get()实际是Thread.currentThread().threadLocals这张表里,用tl当 key 取 value。
因为表挂在线程上,所以天然按线程隔离;线程死亡,整张表随之可回收。
三、为什么线程池下会「串数据」+ 泄漏
线程池里的线程不死、被复用。上一个任务 set 的值若没清理,下一个复用同一线程的任务 get 会读到上一个任务残留的值——这是最隐蔽的串号/越权来源(如复用线程拿到上个用户的身份)。
内存泄漏链路:
Thread(池化,长期活着)
└─ ThreadLocalMap(强)
└─ Entry
key = WeakReference(ThreadLocal) ← TL 无其他强引用时可被 GC
value = 你的对象(强引用) ← 线程活着就一直被引用,无法回收ThreadLocal 实例通常是 static,往往不会被回收,弱引用 key 主要是一种保护性兜底;真正的问题是池化线程长期持有 value。
正确姿势:try/finally 一定 remove
tl.set(userId);
try {
handle();
} finally {
tl.remove(); // 无论如何清掉,既防串号又防泄漏
}四、InheritableThreadLocal 的局限
普通 ThreadLocal 子线程拿不到父线程的值。InheritableThreadLocal 在新建子线程时把父线程值拷过去,但线程池线程是预先创建并复用的,不会在每次提交任务时重新拷贝——所以对线程池基本失效。阿里 TransmittableThreadLocal(TTL) 在任务提交/执行时做快照与回放,才解决线程池场景的透传。
五、ThreadLocal vs synchronized
| ThreadLocal | synchronized/Lock | |
|---|---|---|
| 思路 | 空间换时间,每线程一份、不共享 | 时间换空间,串行访问同一份 |
| 适用 | 本该独立的资源(格式化器、上下文) | 必须共享且要互斥的状态 |
| 开销 | 无竞争,但占多份内存、要清理 | 有竞争与等待 |
六、常见坑清单
- 忘记
remove:线程池下数据串号 + 堆占用累积(大量类加载器场景还会连累 Metaspace); - 把它当共享存储,期望别的线程读到——读不到;
- 异步/线程池里用普通或 Inheritable ThreadLocal 透传上下文失效——用 TTL 或显式传参;
- value 是可变共享对象时,多线程 set 进去的若是同一个实例仍会线程不安全(副本隔离的是引用槽位,不会替你深拷贝)。
总结
ThreadLocal 让每个线程在自己的 ThreadLocalMap 里以 ThreadLocal 为 key 存独立副本,用来隔离非线程安全对象和请求上下文,而不是共享数据;Map 挂在 Thread 上、key 是弱引用、value 是强引用,因此在线程不死的线程池里必须在 finally 中 remove(),否则既会让复用线程读到上个任务的残留值(串号/越权),又会让 value 长期被线程引用造成泄漏;InheritableThreadLocal 只在新建线程时拷贝、对池化线程失效,跨池透传要用 TransmittableThreadLocal 或显式传参。