ThreadLocal
ThreadLocal 为每个线程保存一份独立变量副本,常用于线程上下文传递和线程内状态隔离。
它不是解决共享变量竞争的锁,而是让变量不共享。
# 1. 基本用法
private static final ThreadLocal<String> USER_ID = new ThreadLocal<>();
public void handle(String userId) {
try {
USER_ID.set(userId);
doWork();
} finally {
USER_ID.remove();
}
}
读取:
String userId = USER_ID.get();
# 2. 底层模型
每个线程内部有自己的 ThreadLocalMap。
Thread A
└─ ThreadLocalMap
└─ USER_ID -> "1001"
Thread B
└─ ThreadLocalMap
└─ USER_ID -> "2002"
同一个 ThreadLocal 对象,在不同线程中对应不同值。
# 3. 常见场景
- 当前用户 ID。
- TraceId / 请求链路 ID。
- 租户 ID。
- 数据源上下文。
- 事务上下文。
- 日期格式化器旧方案。
例如日志链路:
请求进入 -> set traceId
业务调用 -> 日志自动读取 traceId
请求结束 -> remove traceId
# 4. 内存泄漏风险
线程池中线程会复用,如果不 remove,旧请求数据可能留在线程里。
请求 A 在线程 T 设置 userId=A
请求 A 结束但未 remove
线程 T 被复用处理请求 B
请求 B 可能读到 A 的 userId
必须:
try {
threadLocal.set(value);
doWork();
} finally {
threadLocal.remove();
}
# 5. 弱引用 key
ThreadLocalMap 的 key 是弱引用,但 value 是强引用。
Entry
├─ key: WeakReference<ThreadLocal>
└─ value: Object 强引用
如果 ThreadLocal 对象被回收,但线程长期存活,value 仍可能滞留。因此即使 key 是弱引用,也必须主动 remove。
# 6. 线程池与上下文传递
ThreadLocal 不会自动跨线程传递。
USER_ID.set("1001");
executor.submit(() -> {
USER_ID.get(); // 通常取不到
});
解决思路:
- 显式把上下文作为参数传入。
- 提交任务时包装 Runnable/Callable。
- 使用框架提供的上下文传递能力。
- 谨慎使用
InheritableThreadLocal,线程池下容易不符合预期。
# 7. InheritableThreadLocal
它可以让子线程创建时继承父线程值。
InheritableThreadLocal<String> local = new InheritableThreadLocal<>();
但线程池中的线程通常早已创建,后续任务不是新建子线程,因此继承行为经常不符合预期。
# 8. 使用原则
- 用于线程内上下文,不要当全局变量。
- set 后必须在 finally 中 remove。
- 值对象不要过大。
- 在线程池异步任务中不要假设上下文自动传播。
- 能用方法参数显式传递时,优先显式传递。
# 专家实践与边界
ThreadLocal 解决的是线程内上下文隔离,不是线程间共享。它常用于 traceId、登录用户、租户、事务上下文等“同一线程调用链都要访问”的信息。
Thread
└─ ThreadLocalMap
├─ ThreadLocal A -> valueA
└─ ThreadLocal B -> valueB
| 场景 | 是否适合 | 注意点 |
|---|---|---|
| 请求 traceId | 适合 | 请求结束必须清理 |
| 用户上下文 | 适合 | 线程池复用会串数据 |
| 大对象缓存 | 不适合 | 容易内存泄漏 |
| 跨线程传递 | 默认不适合 | 需要显式包装 |
| 异步任务上下文 | 谨慎 | CompletableFuture 不自动传播 |
在线程池环境中,线程会被复用。如果请求结束不 remove,下一个请求可能读到上一个请求的上下文,这类 bug 隐蔽且危险。
# Tips 快问快答
Q:ThreadLocal 解决线程安全吗? A:它通过线程隔离避免共享,不是锁。
Q:ThreadLocal 的值存在哪里?
A:存在线程自己的 ThreadLocalMap 中。
Q:为什么一定要 remove? A:线程池线程会复用,不 remove 可能造成数据污染和内存泄漏。
Q:ThreadLocal 会自动传给线程池任务吗? A:不会。
Q:InheritableThreadLocal 在线程池里可靠吗? A:通常不可靠,因为线程池线程不是每次任务新建。
Q:ThreadLocalMap key 是弱引用就不会泄漏吗? A:不是,value 仍可能被长期持有。
Q:TraceId 可以用 ThreadLocal 吗? A:可以,但异步任务要处理上下文传递和清理。
Q:能用参数传递还要用 ThreadLocal 吗? A:优先显式参数,ThreadLocal 适合横切上下文。