跳到主要内容

ThreadLocal

问题

ThreadLocal 的实现原理是什么?为什么会发生内存泄漏?如何在线程池中正确使用 ThreadLocal?

答案

ThreadLocal 是什么?

ThreadLocal 提供线程局部变量——每个线程都持有该变量的独立副本,线程之间互不影响。常用于线程隔离场景,如数据库连接、用户上下文、日期格式化等。

ThreadLocalBasic.java
// 定义 ThreadLocal 变量
private static final ThreadLocal<SimpleDateFormat> dateFormat =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

// 每个线程获取自己的 SimpleDateFormat 副本(线程安全)
String date = dateFormat.get().format(new Date());

// 使用完毕后清理(防止内存泄漏)
dateFormat.remove();

底层数据结构

每个 Thread 对象内部持有一个 ThreadLocalMap

Thread 类中的字段
public class Thread {
// 每个线程拥有自己的 ThreadLocalMap
ThreadLocal.ThreadLocalMap threadLocals = null;
}

ThreadLocalMap 结构

ThreadLocalMap 是 ThreadLocal 中的静态内部类,使用开放定址法(线性探测)解决哈希冲突:

ThreadLocalMap 核心结构
static class ThreadLocalMap {
// Entry 的 key 是 WeakReference<ThreadLocal<?>>
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value; // 强引用
Entry(ThreadLocal<?> k, Object v) {
super(k); // key 是弱引用
value = v;
}
}

private Entry[] table; // 哈希表
private int size; // 元素数量
private int threshold; // 扩容阈值(容量的 2/3)
}

核心方法源码解析

get()

ThreadLocal.get(简化)
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = t.threadLocals;
if (map != null) {
// 以当前 ThreadLocal 实例为 key 查找
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
return (T) e.value;
}
}
// map 为空或未找到,初始化
return setInitialValue();
}

set()

ThreadLocal.set(简化)
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = t.threadLocals;
if (map != null)
map.set(this, value); // 以当前 ThreadLocal 为 key 存入
else
createMap(t, value); // 首次使用,创建 ThreadLocalMap
}

remove()

ThreadLocal.remove(简化)
public void remove() {
ThreadLocalMap m = Thread.currentThread().threadLocals;
if (m != null)
m.remove(this); // 清除当前 ThreadLocal 对应的 Entry
}

内存泄漏问题

ThreadLocal 的 key 使用弱引用(WeakReference),value 使用强引用

当 ThreadLocal 变量(栈上的引用)被置为 null 后:

  1. ThreadLocal 对象只剩弱引用,下次 GC 时被回收
  2. Entry 的 key 变为 null(弱引用被回收)
  3. 但 Entry 的 value 仍被 ThreadLocalMap 强引用 → 无法回收内存泄漏
线程池 + ThreadLocal = 最容易泄漏

线程池中的线程不会销毁,ThreadLocalMap 的生命周期与线程一样长。如果不手动 remove(),value 会一直驻留内存。

为什么 key 要设计成弱引用?

如果 key 是强引用,即使外部不再使用 ThreadLocal,由于 ThreadLocalMap 持有强引用,ThreadLocal 对象也无法被 GC。使用弱引用至少让 ThreadLocal 对象能被回收。

ThreadLocalMap 的自清理机制

ThreadLocalMap 在 get()set()remove() 时会探测性清理 key 为 null 的 Entry:

// expungeStaleEntry():清理过期 Entry
// 从当前位置向后扫描,清理 key == null 的 Entry
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
tab[staleSlot].value = null; // 断开 value 引用
tab[staleSlot] = null;
size--;
// ... 继续扫描后续 Entry
}

但这个清理是惰性的、不完全的,不能依赖它来防止泄漏。

最佳实践

ThreadLocalBestPractice.java
public class UserContext {
private static final ThreadLocal<User> currentUser = new ThreadLocal<>();

public static void set(User user) {
currentUser.set(user);
}

public static User get() {
return currentUser.get();
}

// 必须在请求结束时调用
public static void clear() {
currentUser.remove();
}
}

// 在 Filter/Interceptor 中使用
public class UserFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) {
try {
User user = resolveUser(req);
UserContext.set(user);
chain.doFilter(req, resp);
} finally {
UserContext.clear(); // 必须清理!
}
}
}
ThreadLocal 使用规范
  1. 声明为 private static final:避免重复创建,一个 ThreadLocal 实例对应一类线程局部变量
  2. 用完必须 remove():在 finally 中调用,防止内存泄漏
  3. 使用 withInitial():明确初始值,避免 NPE
  4. 线程池场景特别注意:线程复用导致脏数据和内存泄漏

InheritableThreadLocal

普通 ThreadLocal 在子线程中无法获取父线程的值。InheritableThreadLocal 支持子线程继承父线程的值:

InheritableThreadLocal
InheritableThreadLocal<String> itl = new InheritableThreadLocal<>();
itl.set("父线程的值");

new Thread(() -> {
System.out.println(itl.get()); // "父线程的值" —— 子线程可以获取
}).start();

原理:创建子线程时,Thread.init() 会将父线程的 inheritableThreadLocals 浅拷贝到子线程。

InheritableThreadLocal + 线程池

线程池中线程是复用的,不是每次都新建,所以 InheritableThreadLocal 在线程池场景下可能失效(只在线程创建时继承,不会在每次提交任务时更新)。

解决方案:使用阿里开源的 TransmittableThreadLocal (TTL),支持线程池场景下的值传递。


常见面试问题

Q1: ThreadLocal 的实现原理?

答案

每个 Thread 对象内部维护一个 ThreadLocalMapThreadLocal.set() 以 ThreadLocal 实例自身为 key,将值存入当前线程的 ThreadLocalMap。ThreadLocal.get() 从当前线程的 ThreadLocalMap 中取值。

ThreadLocalMap 使用数组 + 开放定址法(线性探测),Entry 的 key 是 ThreadLocal 的弱引用,value 是强引用。

Q2: ThreadLocal 为什么会内存泄漏?如何避免?

答案

泄漏原因:ThreadLocalMap 的 Entry key 是弱引用,ThreadLocal 对象被 GC 后 key 变为 null,但 value 仍被强引用无法回收。在线程池场景下线程长期存活,积累的无用 value 造成内存泄漏。

避免方式:使用完毕后必须调用 remove(),通常在 try-finally 中执行。

Q3: key 为什么用弱引用而不是强引用?

答案

如果 key 用强引用,即使外部已经不持有 ThreadLocal 的引用(threadLocal = null),由于 ThreadLocalMap 的 Entry 还持有强引用,ThreadLocal 对象和 value 都无法被 GC,造成更严重的泄漏。

使用弱引用后,至少 ThreadLocal 对象能被回收,ThreadLocalMap 的自清理机制(expungeStaleEntry)也有机会清理失效的 Entry。

Q4: ThreadLocalMap 的哈希冲突如何解决?

答案

使用开放定址法(线性探测),而不是 HashMap 的链地址法。

原因:ThreadLocal 通常数量不多,线性探测实现简单、缓存友好(连续内存访问)。每个 ThreadLocal 有一个固定的 threadLocalHashCode(通过黄金分割数 0x61c88647 生成),散列效果好,冲突概率低。

Q5: InheritableThreadLocal 的局限性?

答案

InheritableThreadLocal 只在子线程创建时从父线程拷贝值。在线程池场景下,线程是复用的,不会每次执行任务时重新继承父线程的值。

解决方案:使用阿里的 TransmittableThreadLocal,通过装饰 Runnable/Callable 或 Agent 字节码增强,在任务提交时捕获、在任务执行时恢复 ThreadLocal 值。

Q6: ThreadLocal 的常见应用场景?

答案

  1. 数据库连接管理:每个线程一个 Connection,保证事务隔离
  2. 用户上下文传递:在 Filter 中设置当前用户,后续 Service 层直接获取
  3. 日期格式化:SimpleDateFormat 非线程安全,每线程一个实例
  4. 事务管理:Spring 的 @Transactional 底层使用 ThreadLocal 保存当前事务
  5. MDC 日志追踪:SLF4J 的 MDC 基于 ThreadLocal 实现请求链路追踪

相关链接