ThreadLocal
一句话总结
ThreadLocal 用于为每个线程 保存一份互相隔离 的变量副本:同一个 ThreadLocal 在不同线程里 get() 到的是不同值。其底层是每个 Thread 维护一个 ThreadLocalMap,以 ThreadLocal 实例作为 key(弱引用),以实际数据作为 value。在线程池场景必须 remove(),否则可能发生"串数据"和内存泄漏。
1. 适用场景(什么时候用)
适合"线程维度"的上下文数据:
- 请求上下文 :
traceId/requestId/ 当前登录用户信息(仅限当前线程内) - 资源句柄绑定到线程:数据库连接、事务上下文、会话对象(常见于框架内部实现)
- 跨方法传参:避免层层方法增加参数(但要克制,防止隐式依赖)
- 非线程安全对象的线程隔离 :如
SimpleDateFormat(更推荐DateTimeFormatter)
不适合:
- 需要在线程之间共享/汇总的数据(ThreadLocal 天生隔离)
- 依赖线程池但又不清理的上下文(容易污染复用线程)
2. 基本用法(务必带清理)
2.1 最小示例
java
public class ThreadLocalDemo {
private static final ThreadLocal<Integer> TL = ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
TL.set(100);
try {
System.out.println(TL.get());
} finally {
// 线程池/长生命周期线程中必须清理
TL.remove();
}
}
}
2.2 Web 请求:Filter / Interceptor 中 set 与 finally 清理
java
public final class RequestContext {
private RequestContext() {}
private static final ThreadLocal<String> TRACE_ID = new ThreadLocal<>();
public static void setTraceId(String id) { TRACE_ID.set(id); }
public static String getTraceId() { return TRACE_ID.get(); }
public static void clear() { TRACE_ID.remove(); }
}
// 伪代码:Filter / Interceptor
try {
RequestContext.setTraceId(traceId);
chain.doFilter(req, resp);
} finally {
RequestContext.clear();
}
3. 实现原理(面试核心)
3.1 核心结构:Thread 持有 ThreadLocalMap
- 不是
ThreadLocal自己存值 - 而是
Thread内部有一个ThreadLocalMap,以ThreadLocal为 key 存储 value
概念示意:
Thread
└── ThreadLocalMap
├── Entry(key=WeakReference<ThreadLocal>, value=Object)
├── Entry(...)
└── ...
3.2 set / get / remove 的关键流程
-
set(value)- 取当前线程
Thread.currentThread() - 拿到该线程的
ThreadLocalMap(没有则创建) - 以"当前 ThreadLocal 实例"为 key 写入 value
- 取当前线程
-
get()- 取当前线程的
ThreadLocalMap - 用"当前 ThreadLocal 实例"为 key 查找
- 若没有命中:调用
initialValue()/withInitial(...)生成并写入,再返回
- 取当前线程的
-
remove()- 删除当前线程
ThreadLocalMap中对应 key 的 Entry
- 删除当前线程
4. 为什么会内存泄漏?(以及为什么 key 用弱引用仍可能泄漏)
4.1 关键点
ThreadLocalMap.Entry 的 key 是 WeakReference<ThreadLocal<?>>。
- 当
ThreadLocal实例没有强引用 后,key 可能被 GC 回收,变成null - 但 value 是强引用 ,仍然挂在当前线程的
ThreadLocalMap里 - 如果线程是长生命周期(尤其线程池工作线程),value 可能长期无法释放
这类 Entry 常被称为 stale entry(陈旧条目) :key == null 但 value != null。
4.2 为什么"看起来弱引用了"还要 remove?
因为:
- GC 只能回收 key(ThreadLocal 对象),回收不了 value
ThreadLocalMap会在set/get/remove时顺带做一些清理,但并不可靠/不及时- 线程池线程复用导致 value 长时间存活,甚至"下一个任务读到上一个任务的上下文"(串数据)
4.3 结论
- 业务代码原则:set 后必须 remove(try/finally)
- 特别是在 线程池、Web 容器线程、RPC 框架线程等长生命周期线程中
5. 与 synchronized / Lock 的对比
| 维度 | ThreadLocal | synchronized / Lock |
|---|---|---|
| 思路 | 空间换时间:每线程一份副本 | 时间换空间:共享数据加锁 |
| 数据可见性 | 线程隔离,不共享 | 多线程共享同一份数据 |
| 性能 | 无锁访问,通常更快 | 有锁竞争与上下文切换开销 |
| 适用场景 | 线程上下文、线程绑定资源 | 共享资源并发读写 |
| 风险 | 线程池污染、内存泄漏(需清理) | 死锁、锁竞争、性能抖动 |
6. InheritableThreadLocal 与线程池"上下文传递"
6.1 InheritableThreadLocal
- 作用:子线程创建时,从父线程拷贝一份初始值
- 局限:
- 只在"创建子线程"那一刻拷贝
- 在线程池里线程通常提前创建并复用,所以几乎不生效,甚至造成误解
6.2 线程池中的上下文传递(常见方案)
- 装饰
Runnable/Callable:提交任务前捕获上下文,执行后清理 - 使用成熟方案(如阿里 TTL:
TransmittableThreadLocal)- 解决"线程池复用"导致的上下文无法自动传递的问题
- 仍然要理解其原理与清理策略,避免滥用
7. 最佳实践(直接背)
- 必须
try/finally调用remove()(尤其线程池)。 - ThreadLocal 建议定义为
private static final(同一语义复用同一个 ThreadLocal 实例)。 - value 尽量小、生命周期短,避免缓存大对象。
- 不要把 ThreadLocal 当"全局变量/隐式参数"滥用:会让代码依赖关系变隐蔽,难测试。
- 框架层(Filter/Interceptor/AOP)统一管理 set/clear,业务代码尽量只读。
- 出现"用户串号/traceId 混乱/数据偶现"优先排查 ThreadLocal 是否在线程池中未清理。
8. 高频面试问答
Q1:ThreadLocal 是怎么做到线程隔离的?
每个 Thread 内部维护一个 ThreadLocalMap,同一个 ThreadLocal 在不同线程里对应不同 Entry/value,因此天然隔离。
Q2:ThreadLocal 的 key 为什么用弱引用?
为了避免 ThreadLocal 对象本身无法回收:当外部不再持有 ThreadLocal 的强引用时,弱引用 key 可被 GC。
Q3:key 是弱引用为什么还会内存泄漏?
因为 value 仍是强引用,且挂在长生命周期线程的 ThreadLocalMap 上;key 被回收后会留下 stale entry,若没有及时清理(remove 或 map 自清理触发),value 仍可能长期占用内存。
Q4:线程池里 ThreadLocal 的典型问题是什么?
- 上下文污染/串数据:线程复用导致上一次任务的 value 被下一次任务读到
- 内存泄漏:value 长期挂在线程池工作线程上
解决:任务执行完 finally remove,或使用上下文传递封装/TTL 并配合清理。
Q5:ThreadLocalMap 是怎么处理哈希冲突的?
采用开放寻址(线性探测)方式:发生冲突就向后寻找空槽位;并在访问过程中机会性清理 stale entry。