1. ThreadLocal 底层存储结构
1.1 基本概念
每个线程都有自己的变量副本,各个线程之间相互隔离。
1.2 ThreadLocalMap 的结构

每个线程内部有一个 ThreadLocalMap,本质上是一个 HashMap(Entry数组存数据):
- Entry 的 key :ThreadLocal 对象的弱引用(WeakReference)
- Entry 的 value:要保存的资源对象
以当前的 ThreadLocal 对象为 key,放到当前线程的 ThreadLocalMap 中。
1.3 static 修饰的 ThreadLocal
开发中一般把 ThreadLocal 声明为 static 变量:
- ThreadLocal 对象是所有线程共享的(作为 key)
- 但每个线程的 ThreadLocalMap 是各自独立的
- 不同线程从各自的 map 中取出的 value 是不同的
key 是同一个 ThreadLocal 对象,但每个线程有自己的 map,所以 value 互不干扰。
2. 为什么key是弱引用

2.1 内存泄漏的背景
正常 new 一个对象时,基本都是局部变量,方法执行完 GC 就清理了。
但 ThreadLocal 的内存泄漏比较特殊,需要开发人员操心。
2.2 线程池是核心原因
- 如果直接
new Thread(),线程结束后,Thread 类中的 threadLocals 属性也会被 GC 回收 - 但项目中没人直接 new Thread,都是用线程池(tomcat)
- 线程池中的线程不会销毁,会一直复用
- 线程一直存活 → ThreadLocalMap 一直存在 → Entry 一直存在
2.3 引用链分析
引用关系:Thread → ThreadLocalMap → Entry → key(ThreadLocal)
即便我们不想用 ThreadLocal 了,把 ThreadLocal 引用设为 null:
- Entry 对 key 的引用还是存在的(强引用)
- ThreadLocal 对象无法被 GC 回收
- 造成内存泄漏
2.4 弱引用的作用
把 key 设置为弱引用(WeakReference):
- 只要发生垃圾回收,弱引用指向的对象就会被回收
- key 被回收后变成 null,Entry 变成
null → value的结构 - 从而避免 key 的内存泄漏
2.5 GC后key一定会被回收吗
不一定 。弱引用被 GC 回收的前提是:没有强引用指向该对象。
- 如果 ThreadLocal 对象没有被设置为 null,还有强引用指向它
- 那即使是弱引用,也不会被回收
- 所以必须把 ThreadLocal 对象设为 null,只剩下 Entry key 的弱引用时,GC 才能回收
3. key为null的Entry如何清理
3.1 自动清理机制
ThreadLocalMap 中会不会有大量 key 为 null 的数据?
不会 。当调用 ThreadLocal 的 get()、set()、remove() 方法时:
- 会遍历 Entry 数组
- 清理掉 key 为 null 的无效 Entry
- 通过线性探测法重新处理哈希冲突(重新安置Entry)
3.2 线性探测法
处理哈希冲突有很多种方法,最常见的是 HashMap 的数组+链表(链地址法)。
ThreadLocalMap 也是哈希 Map,但用的是线性探测法:
- 计算出的数组位置已经有数据了 → 就往数组的下一个位置放
- 依次往后找,直到找到空位
4. 为什么value不是弱引用
4.1 value必须是强引用
value 不能是弱引用,原因是:
- value 是为了后面去用它
- 如果 value 是弱引用,暂时没有强引用指向它时,GC 就会把它回收
- 但后续代码可能还要获取使用这个 value
- value 被回收了就没法用了
所以 value 必须是强引用,确保在需要的时候能取到。
4.2 带来的问题
value 是强引用的情况下:
- 即使 key 被回收了(变成 null)
- value 也会一直存在
- 引用链:
Thread → ThreadLocalMap → Entry → value - 导致 value 的内存泄漏
5. ThreadLocal 内存泄漏
5.1 内存泄漏的根本原因
线程池 + 强引用链 共同导致:
- 线程池中的线程长期存活
- ThreadLocalMap 一直存在
- key 被 GC 回收变成 null
- value 是强引用,一直被 Entry 持有
- value 永远无法被回收 → 内存泄漏
5.2 解决方案
使用完 ThreadLocal 之后,手动调用 remove() 方法:
- 手动断开 value 的强引用
- 彻底清理 Entry
- 这是最稳妥、最规范的做法
最佳实践:try-finally 中 finally 块调用 remove()。
6. ThreadLocal vs Synchronized
6.1 核心区别
| 特性 | synchronized | ThreadLocal |
|---|---|---|
| 思想 | 互斥访问,同一时间只有一个线程能访问 | 资源复制,每个线程一份副本,各用各的 |
| 数据共享 | 多线程共享同一份数据 | 多线程各有独立副本,不共享 |
| 并发安全方式 | 排队等待,串行执行 | 空间换时间,并行执行 |
| 适用场景 | 多线程需要修改共享数据 | 每个线程需要独立的数据副本 |
7. 死锁与ThreadLocal思想
7.1 死锁的四个必要条件
操作系统中死锁的四个必要条件:
- 互斥条件:共享资源必须互斥访问
- 请求与保持条件:线程占有资源,同时请求其他资源
- 不剥夺条件:已获得的资源不能被强行剥夺
- 循环等待条件:线程之间形成循环等待资源的关系
7.2 ThreadLocal 如何打破死锁
ThreadLocal 的思想也是处理死锁的一种方式:
- 打破互斥条件:让每个线程操作自己的数据副本,大家各用各的
- 不需要共享资源,自然就不存在互斥问题
- 从根源上避免了死锁
7.3 其他死锁解决方案
打破请求与保持条件 ------ tryLock 方式:
- 使用
ReentrantLock.tryLock() - 加锁失败就可以做其他操作
- 主动释放自己已持有的锁
- 比如:线程一持有锁A,尝试获取锁B失败 → 释放锁A → 重试
- 从而解除死锁
8. 总结对比
8.1 ThreadLocal 核心问答
| 问题 | 答案 |
|---|---|
| 数据存在哪? | 每个线程的 ThreadLocalMap 中,以 ThreadLocal 对象为 key |
| key 为什么是弱引用? | 避免 key 的内存泄漏,GC 时自动回收 key |
| key 被 GC 后一定回收吗? | 不一定,必须没有强引用指向 ThreadLocal 对象才行 |
| value 为什么不是弱引用? | value 后面还要用,如果是弱引用会被误回收 |
| 内存泄漏的根本原因? | 线程池长期存活 + value 强引用链导致 value 无法回收 |
| 怎么防止内存泄漏? | 使用完手动调用 remove() 方法 |
| key 为 null 的 Entry 怎么清理? | get/set/remove 时自动清理,用线性探测法重排 |
8.2 ThreadLocal vs 锁
| 对比维度 | synchronized | ThreadLocal |
|---|---|---|
| 思路 | 互斥,串行 | 复制,并行 |
| 共享性 | 数据共享 | 数据隔离 |
| 空间 | 节省空间 | 空间换时间 |
| 时间 | 时间换空间 | 节省时间 |
| 死锁 | 可能死锁 | 打破互斥条件,避免死锁 |
8.3 核心结论
ThreadLocal 的设计精髓:弱引用的 key + 强引用的 value + 自动清理机制。