ThreadLocal 提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量都不同。ThreadLocal 相当于提供了一种线程隔离,将变量与线程相绑定。

“线程隔离”是效果,原理要回答四个问题:副本存在哪(数据结构)、为什么用弱引用(内存设计)、内存泄漏怎么发生(使用陷阱)、线程池场景为何尤其危险。本文逐层拆解。

存储结构:Map 挂在线程上

关键设计:不是 ThreadLocal 持有 Map,而是每个 Thread 对象持有一个 ThreadLocalMap:

  • Thread.threadLocals 字段即 ThreadLocalMap
  • Map 的 key 是 ThreadLocal 实例(弱引用),value 是副本值(强引用)
  • set/get 时以当前 ThreadLocal 为 key 操作当前线程自己的 Map,天然无锁、无竞争
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // t.threadLocals
if (map != null) map.set(this, value); // this = 当前 ThreadLocal 作 key
else createMap(t, value);
}

这个方向(Map 在线程侧)意味着:线程死亡,Map 随之回收;一个线程用 N 个 ThreadLocal,就是 Map 里 N 个 entry。

弱引用 key 的设计意图与泄漏真相

ThreadLocalMap.Entry 继承 WeakReference<ThreadLocal<?>>:key 是弱引用,value 是强引用。

设计意图:外部强引用消失后(ThreadLocal 变量置 null/出作用域),key 可被 GC 回收,避免”ThreadLocal 对象被 Map 拴住永不回收”。

但泄漏恰恰发生在回收之后:key 被回收变成 null,value 仍被 entry 强引用,而 entry 被 ThreadLocalMap 引用、Map 被线程引用——只要线程活着(线程池里是长期活着),这条链就让 value 无法回收,且再也无法通过 get 访问(key 为 null),成为”够不着的垃圾”。

Thread → ThreadLocalMap → Entry(key=null) → value  ← 泄漏链

所以规范只有一条:**用完必须 remove()**。remove 会清理 entry 本身,斩断整条链。set/get 内部的启发式清理(探测到 null key entry 时顺手清除)只是兜底,不能依赖。

线程池场景:双重风险

池化线程复用把 ThreadLocal 的两个问题放大:

  • 泄漏累积:线程不死亡,未 remove 的 value 永久驻留,池里每个线程各漏一份
  • 数据串味:上一个任务 set 的值未 remove,下一个复用同线程的任务 get 到脏数据(上下文错乱,如用户 A 的请求信息出现在用户 B 的日志里)

因此线程池 + ThreadLocal 的铁律是 try-finally remove:

CONTEXT.set(buildContext());
try {
business();
} finally {
CONTEXT.remove(); // 不可省略
}

变种与选型

  • InheritableThreadLocal:创建子线程时复制父线程 Map,支持父子传递;但对线程池无效(线程早已创建)
  • TransmittableThreadLocal(TTL,阿里开源):配合装饰后的线程池在任务提交时捕获、执行时回放上下文,解决池化传递问题
  • 阿里巴巴 Java 手册明确禁止用 ThreadLocal 传递跨线程业务数据而不清理

选型对照:

  • 线程内跨方法传递上下文(traceId、登录用户、事务连接)→ ThreadLocal + finally remove
  • 跨线程池传递 → TTL 或显式参数传递(最可靠)
  • 每线程一份昂贵对象(SimpleDateFormat、连接)→ ThreadLocal 或池化替代;注意 SimpleDateFormat 的 ThreadLocal 缓存是经典用法

常见面试追问速答

  • 为什么 key 用弱引用?避免 ThreadLocal 对象被 Map 强引用无法回收;但 value 泄漏仍需 remove 解决
  • ThreadLocalMap 为何不用 HashMap?定制实现:线性探测(无链表)、entry 直接数组、清理策略内建,且无需同步(线程私有)
  • 哈希冲突怎么办?ThreadLocal 的 threadLocalHashCode 按黄金分割增量递增,冲突时线性探测下一个槽

ThreadLocal 的原理一句话:把并发问题转化为空间隔离——每线程一份副本,自然无竞争;而它全部的坑(泄漏、串味)都来自”副本生命周期与线程生命周期绑定”这一设计的反面。理解存储方向(Map 在线程侧)与引用设计(key 弱 value 强),使用规范就不再是死记硬背。