ThreadLocal
ThreadLocal 有什么用?
通常情况下,我们创建的变量可以被任何一个线程访问和修改。这在多线程环境中可能导致数据竞争和线程安全问题。那么,如果想让每个线程都有自己的专属本地变量,该如何实现呢?
JDK 中提供的 ThreadLocal 类正是为了解决这个问题。ThreadLocal 类允许每个线程绑定自己的值,可以将其形象地比喻为一个“存放数据的盒子”。每个线程都有自己独立的盒子,用于存储私有数据,确保不同线程之间的数据互不干扰。
当你创建一个 ThreadLocal 变量时,每个访问该变量的线程都会拥有一个独立的副本。这也是 ThreadLocal 名称的由来。线程可以通过 get() 方法获取自己线程的本地副本,或通过 set() 方法修改该副本的值,从而避免了线程安全问题。
举个简单的例子:假设有两个人去宝屋收集宝物。如果他们共用一个袋子,必然会产生争执;但如果每个人都有一个独立的袋子,就不会有这个问题。如果将这两个人比作线程,那么 ThreadLocal 就是用来避免这两个线程竞争同一个资源的方法。
public class ThreadLocalExample {
private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
Runnable task = () -> {
int value = threadLocal.get();
value += 1;
threadLocal.set(value);
System.out.println(Thread.currentThread().getName() + " Value: " + threadLocal.get());
};
Thread thread1 = new Thread(task, "Thread-1");
Thread thread2 = new Thread(task, "Thread-2");
thread1.start(); // 输出: Thread-1 Value: 1
thread2.start(); // 输出: Thread-2 Value: 1
}
}⭐️ThreadLocal 原理了解吗?
从 Thread类源代码入手。
public class Thread implements Runnable {
//......
//与此线程有关的ThreadLocal值。由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null;
//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
//......
}从上面Thread类 源代码可以看出Thread 类中有一个 threadLocals 和 一个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量,我们可以把 ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap。默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal 类的 set或get方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的 get()、set()方法。
ThreadLocal类的set()方法
public void set(T value) {
//获取当前请求的线程
Thread t = Thread.currentThread();
//取出 Thread 类内部的 threadLocals 变量(哈希表结构)
ThreadLocalMap map = getMap(t);
if (map != null)
// 将需要存储的值放入到这个哈希表中
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}通过上面这些内容,我们足以通过猜测得出结论:最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上,ThreadLocal 可以理解为只是ThreadLocalMap的封装,传递了变量值。 ThrealLocal 类中可以通过Thread.currentThread()获取到当前线程对象后,直接通过getMap(Thread t)可以访问到该线程的ThreadLocalMap对象。
每个Thread中都具备一个ThreadLocalMap,而ThreadLocalMap可以存储以ThreadLocal为 key ,Object 对象为 value 的键值对。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
//......
}比如我们在同一个线程中声明了两个 ThreadLocal 对象的话, Thread内部都是使用仅有的那个ThreadLocalMap 存放数据的,ThreadLocalMap的 key 就是 ThreadLocal对象,value 就是 ThreadLocal 对象调用set方法设置的值。
ThreadLocal 数据结构如下图所示:

ThreadLocalMap是ThreadLocal的静态内部类。

⭐️ThreadLocal 内存泄露问题是怎么导致的?
ThreadLocal 内存泄漏的根本原因在于其内部实现机制。
通过上面的内容我们已经知道:每个线程维护一个名为 ThreadLocalMap 的 map。 当你使用 ThreadLocal 存储值时,实际上是将值存储在当前线程的 ThreadLocalMap 中,其中 ThreadLocal 实例本身作为 key,而你要存储的值作为 value。
ThreadLocal 的 set() 方法源码如下:
public void set(T value) {
Thread t = Thread.currentThread(); // 获取当前线程
ThreadLocalMap map = getMap(t); // 获取当前线程的 ThreadLocalMap
if (map != null) {
map.set(this, value); // 设置值
} else {
createMap(t, value); // 创建新的 ThreadLocalMap
}
}ThreadLocalMap 的 set() 和 createMap() 方法中,并没有直接存储 ThreadLocal 对象本身,而是使用 ThreadLocal 的哈希值计算数组索引,最终存储于类型为static class Entry extends WeakReference<ThreadLocal<?>>的数组中。
int i = key.threadLocalHashCode & (len-1);ThreadLocalMap 的 Entry 定义如下:
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}ThreadLocalMap 的 key 和 value 引用机制:
- key 是弱引用:
ThreadLocalMap中的 key 是ThreadLocal的弱引用 (WeakReference<ThreadLocal<?>>)。 这意味着,如果ThreadLocal实例不再被任何强引用指向,垃圾回收器会在下次 GC 时回收该实例,导致ThreadLocalMap中对应的 key 变为null。 - value 是强引用:即使
key被 GC 回收,value仍然被ThreadLocalMap.Entry强引用存在,无法被 GC 回收。
当 ThreadLocal 实例失去强引用后,其对应的 value 仍然存在于 ThreadLocalMap 中,因为 Entry 对象强引用了它。如果线程持续存活(例如线程池中的线程),ThreadLocalMap 也会一直存在,导致 key 为 null 的 entry 无法被垃圾回收,即会造成内存泄漏。
也就是说,内存泄漏的发生需要同时满足两个条件:
ThreadLocal实例不再被强引用;- 线程持续存活,导致
ThreadLocalMap长期存在。
虽然 ThreadLocalMap 在 get(), set() 和 remove() 操作时会尝试清理 key 为 null 的 entry,但这种清理机制是被动的,并不完全可靠。
如何避免内存泄漏的发生?
- 在使用完
ThreadLocal后,务必调用remove()方法。 这是最安全和最推荐的做法。remove()方法会从ThreadLocalMap中显式地移除对应的 entry,彻底解决内存泄漏的风险。 即使将ThreadLocal定义为static final,也强烈建议在每次使用后调用remove()。 - 在线程池等线程复用的场景下,使用
try-finally块可以确保即使发生异常,remove()方法也一定会被执行。
为什么 Entry 的 key 要设计为弱引用?
这是一个经典的面试追问。很多同学知道 ThreadLocalMap 的 key 是弱引用,但不清楚为什么要这样设计,以及如果换成强引用会怎样。
我们先来看完整的引用链路。当一个线程使用 ThreadLocal 时,涉及以下引用关系:
强引用(栈/静态变量)──→ ThreadLocal 实例
↑
Thread ──→ ThreadLocalMap ──→ Entry ─── key(WeakReference)──┘
│
└─── value(强引用)──→ 实际存储的对象理解了这条引用链路,我们来对比两种设计方案:
假设 key 使用强引用(实际没有采用):
当业务代码中的 ThreadLocal 引用被置为 null(例如方法执行结束、对象被回收),此时虽然业务代码已经不再需要这个 ThreadLocal,但由于 ThreadLocalMap 的 Entry 对 key 持有强引用,ThreadLocal 实例仍然无法被 GC 回收。只要线程不终止,这个 ThreadLocal 和它对应的 value 都会一直存在于内存中,造成 key 和 value 都无法回收的内存泄漏。
key 使用弱引用(实际采用的方案):
当业务代码中的 ThreadLocal 引用被置为 null 后,由于 Entry 的 key 是弱引用,ThreadLocal 实例在下次 GC 时会被回收,key 变为 null。此时虽然 value 仍然存在(强引用),但 ThreadLocalMap 在执行 get()、set()、remove() 等操作时,会主动探测并清理这些 key 为 null 的 “stale entry”(过期条目),从而释放 value 对象。
也就是说,弱引用的设计是一种”兜底”防御机制——即便开发者忘记调用 remove(),JVM 的 GC 配合 ThreadLocalMap 的自清理逻辑,仍然有机会回收泄漏的数据。而如果使用强引用,一旦忘记 remove(),就完全没有任何补救机会了。
需要注意的是,这种自清理机制是被动触发的(只在
get/set/remove操作时顺便清理),并不能保证所有过期条目都被及时清理。因此,弱引用只是降低了内存泄漏的风险,并没有彻底消除它,手动调用remove()仍然是必须的。
线程池场景下的特殊风险
上面提到内存泄漏的条件之一是”线程持续存活”。在使用 new Thread() 创建线程的场景下,线程执行完毕后会被销毁,其持有的 ThreadLocalMap 也会随之被 GC 回收,泄漏的影响相对有限。
但在线程池场景下,问题会被严重放大。线程池中的核心线程默认不会被销毁,它们会被反复复用来执行不同的任务。这意味着:
- 内存泄漏持续累积:每个任务如果使用了
ThreadLocal却没有清理,其 value 就会一直残留在该线程的ThreadLocalMap中。随着任务不断提交和执行,泄漏的数据会越积越多,最终可能导致 OOM。 - 数据污染(脏数据):上一个任务设置的
ThreadLocal值,如果没有被清理,下一个被分配到同一线程的任务就能读取到这个残留值。这可能导致严重的业务逻辑错误,比如用户 A 的请求读取到了用户 B 的身份信息。
美团技术团队的真实事故案例:
美团技术团队在《Java 线程池实现原理及其在美团业务中的实践》一文中就记录了一次因 ThreadLocal 使用不当引发的线上事故:在一个依赖 ThreadLocal 传递用户上下文的 Web 应用中,由于使用了线程池处理请求,且没有在请求结束后清理 ThreadLocal,导致后续请求复用了同一线程时,读取到了前一个请求遗留的用户信息,造成了用户数据串号的严重问题。
阿里巴巴 Java 开发手册的强制规约
正因为线程池 + ThreadLocal 的组合如此容易踩坑,《阿里巴巴 Java 开发手册》在”并发处理”章节中对此做出了强制级别的要求:
【强制】 必须回收自定义的
ThreadLocal变量记录的当前线程的值,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用try-finally块进行回收。
正确的使用模式如下:
// 定义为 static final,避免重复创建 ThreadLocal 实例
private static final ThreadLocal<UserContext> userContextHolder = new ThreadLocal<>();
public void processRequest(HttpServletRequest request) {
try {
// 在 try 块中设置值
UserContext context = buildUserContext(request);
userContextHolder.set(context);
// 执行业务逻辑
doBusinessLogic();
} finally {
// 在 finally 块中必须清理,确保无论是否发生异常都会执行
userContextHolder.remove();
}
}这里有三个关键要点:
ThreadLocal声明为static final:确保整个应用只有一个ThreadLocal实例,避免因重复创建导致旧实例失去强引用后 key 被回收,加剧内存泄漏。try-finally保证remove()一定被执行:即使业务逻辑抛出异常,finally块也能确保ThreadLocal被清理。- 在使用完毕后立即清理,而不是在下次使用前设置:在使用前
set()虽然可以覆盖旧值解决脏数据问题,但无法解决上一次任务遗留 value 的内存占用问题。只有在用完后remove(),才能同时避免内存泄漏和数据污染。
⭐️如何跨线程传递 ThreadLocal 的值?
为什么 ThreadLocal 在异步场景下会失效?
ThreadLocal 的值不在 ThreadLocal 对象中,而是存储在 Thread 里:
Thread → ThreadLocalMap → Entry(ThreadLocal, value)ThreadLocal 数据结构如下图所示:

异步执行往往意味着任务会从当前线程切换到另一个线程(例如线程池中的工作线程)执行。由于不同线程各自维护独立的 ThreadLocalMap,默认情况下 ThreadLocal 的上下文无法在异步执行中自动传递。
如何跨线程传递 ThreadLocal 的值?
为了解决这个问题,业界有两套主流的解决方案,一套是 JDK 原生的,另一套是阿里巴巴开源的。
InheritableThreadLocal:JDK1.2 提供的一个类,继承自ThreadLocal。使用InheritableThreadLocal时,会在创建子线程时,令子线程继承父线程中的ThreadLocal值,但是无法支持线程池场景下的ThreadLocal值传递。TransmittableThreadLocal:TransmittableThreadLocal(简称 TTL) 是阿里巴巴开源的工具类,继承并加强了InheritableThreadLocal类,可以在线程池的场景下支持ThreadLocal值传递。项目地址:https://github.com/alibaba/transmittable-thread-local。
InheritableThreadLocal 原理
InheritableThreadLocal 实现了创建异步线程时,继承父线程 ThreadLocal 值的功能。该类是 JDK 团队提供的,通过改造 JDK 源码包中的 Thread 类来实现创建线程时,ThreadLocal 值的传递。
InheritableThreadLocal 的值存储在哪里?
在 Thread 类中添加了一个新的 ThreadLocalMap ,命名为 inheritableThreadLocals ,该变量用于存储需要跨线程传递的 ThreadLocal 值。如下:
class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}如何完成 ThreadLocal 值的传递?
通过改造 Thread 类的构造方法来实现,在创建 Thread 线程时,拿到父线程的 inheritableThreadLocals 变量赋值给子线程即可。相关代码如下:
// Thread 的构造方法会调用 init() 方法
private void init(/* ... */) {
// 1、获取父线程
Thread parent = currentThread();
// 2、将父线程的 inheritableThreadLocals 赋值给子线程
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}InheritableThreadLocal 的方案有什么问题?
这个方案的缺陷在于它的一次性,也就是它只在线程创建时发生一次复制。然而,现在的开发中,我们会大量使用线程池,但线程池里的线程是被复用的。
想象一下,任务A在线程1中执行,把它的 ThreadLocal 值传给了线程池里的子线程2。任务A结束后,线程1去休息了。接着,任务B来了,它在线程3中执行,线程池又复用了刚才那个子线程2来执行任务B的一部分。此时,子线程2的ThreadLocal里还残留着任务A传给它的脏数据,而任务B(在线程3里)的上下文却完全没有传递过来。这就导致了数据污染和上下文丢失。
TransmittableThreadLocal 原理
JDK 默认没有支持线程池场景下 ThreadLocal 值传递的功能,因此阿里巴巴开源了一套工具 TransmittableThreadLocal 来实现该功能。
由于阿里巴巴无法改动 JDK 源码,TTL 巧妙地利用了装饰器模式对任务(Runnable/Callable)或线程池(Executor)进行增强,将上下文的传递时机从“线程创建时”延迟到了“任务提交与执行时”。
TTL 的核心逻辑可以概括为三个阶段(CRR):
- Capture(捕获):在提交任务(如调用
execute)的一瞬间,TtlRunnable会调用TransmittableThreadLocal.Transmitter.capture()。它通过内部维护的holder集合,抓取当前父线程中所有活跃的 TTL 变量并存入快照。 - Replay(回放):在线程池的工作线程执行
run()方法前,调用replay()。它将快照中的值set到当前工作线程中,并备份该线程原有的旧值。 - Restore(恢复):任务执行结束后,调用
restore()。它根据备份将工作线程恢复到执行前的状态,防止上下文污染或内存泄漏。
这张图是 TTL 官方提供的 CRR 整个过程的时序图:

不太好理解吧?可以看下我绘制的这张 CRR 时序图,更清晰直观一些:
sequenceDiagram
participant P as 父线程(Submitter)
participant W as TTL 包装器(TtlRunnable / Agent)
participant C as 线程池工作线程(Worker)
Note over P: 1. set context = "A"
P->>W: 2. 提交任务(Capture)
Note right of W: 捕获父线程中所有活跃的 TTL 变量快照
W->>C: 3. 执行任务 run()
Note over C: 4. Replay
Note right of C: 备份工作线程原有 TTL 值<br/>并设置 Capture 得到的值
Note over C: 5. 业务逻辑执行<br/>get context = "A"
Note over C: 6. Restore
Note right of C: 恢复工作线程原有 TTL 值<br/>防止上下文污染
C-->>P: 7. 任务执行结束
也就是说,TTL 的本质是在任务提交时 Capture 上下文,在任务执行前 Replay 上下文,在任务结束后 Restore 线程状态,从而安全地支持线程池中的 ThreadLocal 传递。
TTL 提供了两种主要的接入方式,可根据侵入性要求和改造成本进行选择。
1. 显式包装(手动接入)
使用 TtlRunnable.get(Runnable) 或 TtlCallable.get(Callable) 对任务进行包装,使用 TtlExecutors.getTtlExecutor(Executor)、getTtlExecutorService(...) 对线程池进行包装。这种接入方式清晰可控,但需要业务代码配合,存在一定侵入性。
下面这段代码展示了 TTL 通过 CRR,在支持线程池复用和拒绝策略的前提下,安全地传递并隔离 ThreadLocal 上下文。
public class TtlContextHolder {
private static final Logger log = LoggerFactory.getLogger(TtlContextHolder.class);
// 1. 使用 static final 确保 TTL 实例不被重复创建,防止内存泄漏
// 重写 copy 方法(可选):如果是引用类型,建议实现深拷贝
private static final TransmittableThreadLocal<String> CONTEXT = new TransmittableThreadLocal<String>() {
@Override
public String copy(String parentValue) {
// 默认是直接返回引用,如果是可变对象(如 Map),请在这里 new 新对象
return parentValue;
}
};
// 2. 线程池初始化:确保只被 TtlExecutors 包装一次
private static final ExecutorService TTL_EXECUTOR_SERVICE;
static {
ExecutorService rawExecutor = new ThreadPoolExecutor(
2, 4, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000), (Runnable r) -> new Thread(r, "ttl-worker-" + r.hashCode()),
new ThreadPoolExecutor.CallerRunsPolicy() // 关键:TTL 完美支持此拒绝策略
);
// 包装原始线程池
TTL_EXECUTOR_SERVICE = TtlExecutors.getTtlExecutorService(rawExecutor);
}
public static void main(String[] args) throws Exception {
try {
// 3. 在父线程中设置上下文
CONTEXT.set("value-set-in-parent");
log.info("父线程上下文: {}", CONTEXT.get());
// 4. 使用 Lambda 简化任务提交
TTL_EXECUTOR_SERVICE.submit(() -> {
log.info("异步任务(Runnable)读取上下文: {}", CONTEXT.get());
// 模拟业务逻辑
// 注意:子线程修改是否影响父线程,取决于 copy() 是否做了深拷贝
CONTEXT.set("value-modified-in-child");
});
Future<String> future = TTL_EXECUTOR_SERVICE.submit(() -> {
log.info("异步任务(Callable)读取上下文: {}", CONTEXT.get());
return "Success";
});
future.get();
// 5. 验证父线程上下文是否被污染
log.info("父线程最终上下文: {}", CONTEXT.get());
} finally {
// 6. 清理当前线程(父线程)的上下文,子线程的上下文由 TTL 的 Restore 机制自动恢复
CONTEXT.remove();
}
}
}输出:
09:06:31.438 INFO [main] TtlContextHolder - 父线程上下文: value-set-in-parent
09:06:31.452 INFO [ttl-worker-1663166483] TtlContextHolder - 异步任务(Runnable)读取上下文: value-set-in-parent
09:06:31.453 INFO [ttl-worker-841283083] TtlContextHolder - 异步任务(Callable)读取上下文: value-set-in-parent
09:06:31.453 INFO [main] TtlContextHolder - 父线程最终上下文: value-set-in-parent如果你想要测试这段代码,记得引入 TTL 的 Maven 依赖;
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>transmittable-thread-local</artifactId>
<version>2.14.4</version>
</dependency>2. 无侵入接入(Java Agent)
通过 Java Agent 在类加载阶段对线程池相关类进行 字节码增强,自动织入 TTL 的上下文传递逻辑,实现业务代码零改造的上下文透传。这种方式业务代码无需感知 TTL 的存在,但实现复杂度相对较高。
TTL Agent 默认修饰了以下 JDK 执行器组件:
- 标准线程池:
java.util.concurrent.ThreadPoolExecutor和java.util.concurrent.ScheduledThreadPoolExecutor。 - ForkJoin 体系:
java.util.concurrent.ForkJoinTask(从而透明支持了CompletableFuture和 Java 8 并行流Stream)。 - 遗留组件:
java.util.TimerTask(自 v2.7.0 起支持,v2.11.2 起默认开启)。
在 Java 启动参数中加入 -javaagent 配置:
# 基础配置
java -javaagent:path/to/transmittable-thread-local-2.x.y.jar \
-cp classes \
com.your.app.Main应用场景
- 压测流量标记: 在压测场景中,使用
ThreadLocal存储压测标记,用于区分压测流量和真实流量。如果标记丢失,可能导致压测流量被错误地当成线上流量处理。 - 上下文传递:在分布式系统中,传递链路追踪信息(如 Trace ID)或用户上下文信息。
总结
ThreadLocal 的值默认是无法跨线程传递的,因为它的值是存在每个 Thread 对象自己的 ThreadLocalMap 里的,父子线程是两个不同的对象。
为了解决这个问题,主要有两种方案:
- JDK的 InheritableThreadLocal:它会在创建子线程的时候,把父线程的值复制一份给子线程。但它的问题是,在线程池场景下会失效。因为线程池会复用线程,这会导致线程拿到的可能是上一个任务传下来的脏数据。
- 阿里的 TransmittableThreadLocal (TTL):这是我们项目里用的方案,它专门解决线程池的问题。它的原理是,在提交任务到线程池时,它会把父线程的
ThreadLocal值捕获下来,和任务绑定在一起。等线程池里的某个线程要执行这个任务时,它再把捕获的值设置到这个线程上,任务执行完再清理掉。
简单说,InheritableThreadLocal是跟线程绑定的,只在创建时有效;而TTL是跟任务绑定的,完美支持线程池。
评论
使用 GitHub 账号即可参与加载较慢?可 直接前往 GitHub Discussions 查看与参与。