ThreadLocal 是什么
ThreadLocal 用于保存线程私有变量。
同一个 ThreadLocal 可以被多个线程访问,但每个线程读取和修改的都是自己保存的值,线程之间默认互不影响。
常见用途包括:
- 保存当前用户信息;
- 保存 TraceId;
- 保存租户或请求上下文;
- 在线程内部隐式传递参数。
private static final ThreadLocal<String> USER_CONTEXT =
new ThreadLocal<>();
调用 set() 时,数据并不是直接存入 ThreadLocal 对象,而是存入当前线程内部的 ThreadLocalMap:
Thread
└── ThreadLocalMap
├── ThreadLocal A → Value A
└── ThreadLocal B → Value B
一个线程可以使用多个 ThreadLocal,但这些数据都保存在当前线程自己的 ThreadLocalMap 中。
ThreadLocal 为什么会内存泄漏
ThreadLocalMap 中的 Entry 可以简化为:
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
}
其中:
- Key,也就是
ThreadLocal,是弱引用; - Value 是强引用。
如果外部不再引用某个 ThreadLocal,GC 可以回收 Key,此时 Map 中可能留下:
key = null
value = 业务对象
只要线程仍然存活,Value 就可能继续被 ThreadLocalMap 引用。
在线程池中,工作线程会长期存在并被反复复用,因此未清理的 Value 可能长期无法释放。更危险的是,下一个任务可能读到上一个任务遗留的用户信息、TraceId 或租户信息。JDK 文档也指出,线程本地变量会一直保留到线程结束或调用 remove(),在线程池中可能从一个任务泄漏到另一个任务。
因此,应当始终使用:
try {
USER_CONTEXT.set(userId);
// 业务逻辑
process();
} finally {
USER_CONTEXT.remove();
}
为什么通常定义为 static final
一般建议将 ThreadLocal 定义为:
private static final ThreadLocal<User> USER_CONTEXT =
new ThreadLocal<>();
static final 可以让类持续持有这个 ThreadLocal,避免它作为局部变量使用后失去外部强引用,从而使 Entry 的 Key 变为 null。
但这并不能代替 remove()。
即使 Key 一直存在,Value 仍然会被线程持有。在线程池中,如果任务执行结束后不清理,依然会出现:
- 内存长期占用;
- 线程复用导致的脏数据;
- 用户、租户或链路信息串用。
因此可以记为:
static final:保证 ThreadLocal 入口稳定
finally remove:清理当前线程中的业务数据
InheritableThreadLocal
普通 ThreadLocal 的值不会自动传递给其他线程。
如果父线程通过 new Thread() 创建子线程,可以使用 InheritableThreadLocal:
private static final InheritableThreadLocal<String> CONTEXT =
new InheritableThreadLocal<>();
子线程创建时,会获取父线程中对应变量的初始值。其核心扩展方法是:
protected T childValue(T parentValue)
默认实现直接返回 parentValue,因此父线程和子线程可能持有同一个对象引用。只有需要不同的继承行为时,才需要重写 childValue()。
如果保存的是 String、Integer 等不可变对象,通常不需要重写:
private static final InheritableThreadLocal<String> TRACE_ID =
new InheritableThreadLocal<>();
如果保存的是可变对象,并且希望父子线程相互隔离,可以创建一个副本:
private static final InheritableThreadLocal<Map<String, String>> CONTEXT =
new InheritableThreadLocal<>() {
@Override
protected Map<String, String> childValue(
Map<String, String> parentValue) {
return parentValue == null
? null
: new HashMap<>(parentValue);
}
};
这里的 new HashMap<>(parentValue) 只是复制 Map 容器。
如果 Map 中的 Value 本身也是可变对象,父子线程仍然可能共享这些 Value,需要根据业务情况继续复制。
为什么 InheritableThreadLocal 不适合线程池
InheritableThreadLocal 只在线程创建时传递一次数据。
但线程池中的工作线程通常已经提前创建,后续任务只是在反复复用这些线程。因此,提交新任务时,工作线程不会重新继承提交线程的上下文。
这可能导致:
- 读取不到当前任务提交者的上下文;
- 读取到工作线程创建时的旧值;
- 不同任务之间发生上下文污染。
因此:
new Thread() 创建子线程:
可以考虑 InheritableThreadLocal
线程池复用工作线程:
不能依赖 InheritableThreadLocal
TransmittableThreadLocal
为了解决线程池中的上下文传递问题,阿里巴巴开源了 TransmittableThreadLocal,简称 TTL。
TTL 传递的不是“线程创建时”的上下文,而是任务提交时的上下文。
它可以被理解为三个步骤:
Capture:任务提交时捕获上下文
Replay:任务执行前将上下文放入工作线程
Restore:任务结束后恢复工作线程原来的上下文
阿里巴巴 TTL 官方文档将这个流程称为 Capture、Replay、Restore 模式。
基本定义如下:
private static final TransmittableThreadLocal<String> CONTEXT =
new TransmittableThreadLocal<>();
但只把 ThreadLocal 换成 TTL 还不够,还需要包装线程池或任务。
包装线程池
推荐统一包装 ExecutorService:
ExecutorService originalExecutor =
Executors.newFixedThreadPool(10);
ExecutorService ttlExecutor =
TtlExecutors.getTtlExecutorService(originalExecutor);
之后通过包装后的线程池提交任务:
try {
CONTEXT.set("request-001");
ttlExecutor.submit(() -> {
System.out.println(CONTEXT.get());
});
} finally {
CONTEXT.remove();
}
官方提供的实际方法名是:
TtlExecutors.getTtlExecutorService(executorService)
而不是 TransmittableExecutorService。
包装单个任务
也可以只包装某个 Runnable:
Runnable task = TtlRunnable.get(() -> {
System.out.println(CONTEXT.get());
});
executor.submit(task);
如果同一个原始 Runnable 被多次提交,每次都应重新调用 TtlRunnable.get(),因为包装时捕获的是当次上下文。
通常情况下,统一包装线程池更不容易遗漏。
TTL 的 copy 方法
TTL 提供了:
public T copy(T parentValue)
copy() 用于决定任务创建或提交时,应该如何复制当前上下文。
默认实现直接返回原对象引用,因此 copy() 也不是必须重写。官方 API 明确说明,默认行为是返回源线程中 Value 的引用;只有需要不同传递行为时才覆盖该方法。
不可变对象一般不需要处理:
private static final TransmittableThreadLocal<String> TRACE_ID =
new TransmittableThreadLocal<>();
如果保存的是可变对象,并且希望异步任务读取任务提交时的独立快照,可以重写 copy():
private static final TransmittableThreadLocal<Map<String, String>> CONTEXT =
new TransmittableThreadLocal<>() {
@Override
public Map<String, String> copy(
Map<String, String> parentValue) {
return parentValue == null
? null
: new HashMap<>(parentValue);
}
};
例如:
- 主线程中的配置是 A;
- 主线程提交异步任务;
- 主线程把配置改成 B;
- 异步任务稍后开始运行。
如果希望异步任务仍然使用提交时的配置 A,就需要通过 copy() 创建快照。
三种 ThreadLocal 的区别
| 类型 | 使用场景 | 传递时机 |
|---|---|---|
ThreadLocal | 当前线程内部保存数据 | 不跨线程传递 |
InheritableThreadLocal | 父线程创建新的子线程 | 子线程创建时 |
TransmittableThreadLocal | 线程池和异步任务 | 任务提交、执行时 |
可以简单记为:
线程内部:
ThreadLocal
真正创建新子线程:
InheritableThreadLocal
线程池复用线程:
TransmittableThreadLocal
最佳实践
ThreadLocal通常定义为private static final。set()后必须在finally中执行remove()。- 在线程池中尤其要注意任务之间的数据污染。
InheritableThreadLocal的正确方法名是childValue(),不是init()。TransmittableThreadLocal的复制方法是copy(),不是clone()。childValue()和copy()并非必须重写。- 传递不可变对象时,通常可以共享引用。
- 传递可变对象时,需要考虑防御性复制和线程安全。
- TTL 必须配合
TtlExecutors、TtlRunnable或其他官方包装方式使用。 - 能够清晰地显式传参时,应优先考虑显式传参,避免引入隐藏依赖。
总结
ThreadLocal 的数据保存在当前线程的 ThreadLocalMap 中,而不是直接保存在 ThreadLocal 对象里。
它最重要的使用原则是:
try {
threadLocal.set(value);
// 业务代码
} finally {
threadLocal.remove();
}
InheritableThreadLocal 解决的是创建子线程时的数据继承,TTL 解决的是线程池复用线程时的上下文传递。
对于 childValue() 和 copy(),不应简单记成“必须重写”,更准确的说法是:
不可变对象:
通常可以使用默认的引用传递
可变对象:
根据是否需要隔离或快照,决定是否复制