外观
一句话答案
ThreadLocal 让每个线程持有变量的独立副本,底层用 ThreadLocalMap(key 是弱引用),用完必须 remove 防止内存泄漏。
核心要点
内存泄漏的根因:
Entry:Key(弱引用 → ThreadLocal 对象)+ Value(强引用 → 实际值)强引用:ThreadLocal ref ──────────────────→ ThreadLocal 对象
弱引用:Entry.key ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ → ThreadLocal 对象
当外部 ThreadLocal ref 被置为 null 后:
ThreadLocal 对象仅有弱引用(Entry.key),下次 GC 时被回收
→ Entry.key = null(弱引用断掉)
但 Entry.value 仍然被 ThreadLocalMap 强引用!
Thread → threadLocals → Entry[] → Entry.value(无法被回收)内存泄漏成立的条件:
- ThreadLocal 变量没有外部强引用(被 GC 回收)
- 线程没有结束(如线程池中的线程长期存活)
- Entry.value 是大对象或大量积累
解决方案:
1. 使用完后立刻 remove()(最重要!)
java
ThreadLocal<UserContext> userContext = new ThreadLocal<>();
try {
userContext.set(new UserContext(userId));
// ... 业务逻辑
} finally {
userContext.remove(); // 无论是否异常,都清理!
}2. ThreadLocal 声明为 static(减少实例数量,不根治)
java
// 声明为 static,ThreadLocal 对象有类级别强引用,不会被 GC
// 但仍需 remove(),否则 value 仍然泄漏
private static final ThreadLocal<UserContext> USER_CTX = new ThreadLocal<>();为什么 Entry.key 用弱引用而不用强引用?
- 如果 key 是强引用:即使外部 ThreadLocal 变量置 null,Entry 仍持有强引用,ThreadLocal 对象和 value 都无法被 GC,泄漏更严重
- 弱引用是一种"尽力而为"的设计:至少 ThreadLocal 对象本身可以被 GC
追问与易错
追问方向:
- 为什么 ThreadLocalMap 的 key 是弱引用?(ThreadLocal 对象可以被 GC)
- 既然 key 是弱引用为什么还会泄漏?(value 是强引用,key 被回收后 value 无法清理)
- InheritableThreadLocal 是什么?(子线程继承父线程的 ThreadLocal 值)
易错点:
- ❌ "ThreadLocal 是线程安全的变量"——它是线程隔离的变量,每个线程各自一份
- ❌ "线程池 + ThreadLocal 不会泄漏"——线程复用时旧 value 不会自动清理,必须 remove
💡 记忆锚点
ThreadLocal 像每人一个储物柜(ThreadLocalMap),钥匙(key)是弱引用做的纸牌——钥匙丢了(GC 回收)柜子里的东西(value)还在,又没人来清理,就成了占着柜子的垃圾。线程池里线程不销毁,垃圾越攒越多——所以用完必须 remove(),自己清柜子。