ThreadLocal 原理分析
ThreadLocal 提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量都不同。ThreadLocal 相当于提供了一种线程隔离,将变量与线程相绑定。
“线程隔离”是效果,原理要回答四个问题:副本存在哪(数据结构)、为什么用弱引用(内存设计)、内存泄漏怎么发生(使用陷阱)、线程池场景为何尤其危险。本文逐层拆解。
存储结构:Map 挂在线程上
关键设计:不是 ThreadLocal 持有 Map,而是每个 Thread 对象持有一个 ThreadLocalMap:
Thread.threadLocals字段即 ThreadLocalMap- Map 的 key 是 ThreadLocal 实例(弱引用),value 是副本值(强引用)
- set/get 时以当前 ThreadLocal 为 key 操作当前线程自己的 Map,天然无锁、无竞争
public void set(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()); |
变种与选型
- 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 强),使用规范就不再是死记硬背。











