ThreadLocal的使用和内存泄漏

ThreadLocal的使用和内存泄漏

ThreadLocal 解决了什么问题、会引发什么问题,这是一个经典话题。很多人用 ThreadLocal 存储用户上下文,却忽略了内存泄漏的风险。本文从原理到内存泄漏的处理方式都覆盖到。

ThreadLocal 的基本用法

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 remove() {
currentUser.remove();
}
}

// 使用
UserContext.set(new User("张三"));
User user = UserContext.get(); // 获取当前线程的用户
UserContext.remove(); // 清理

典型应用场景

#

1. 用户登录信息传递

public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
String token = req.getHeader("Authorization");
User user = authService.validate(token);
UserContext.set(user); // 绑定到当前线程
return true;
}

@Override
public void afterCompletion(...) {
UserContext.remove(); // 请求结束,清理
}
}

#

2. 数据库连接管理

public class ConnectionManager {
private static final ThreadLocal<Connection> holder = new ThreadLocal<>();

public static Connection getConnection() throws SQLException {
Connection conn = holder.get();
if (conn == null) {
conn = dataSource.getConnection();
holder.set(conn);
}
return conn;
}
}

#

3. SimpleDateFormat 线程安全

public class DateUtil {
private static final ThreadLocal<SimpleDateFormat> formatter =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

public static String format(Date date) {
return formatter.get().format(date);
}
}

Java 8+ 推荐:使用 DateTimeFormatter,它是线程安全的。

底层实现原理

#

Thread 内部结构

public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}

每个 Thread 对象内部维护一个 ThreadLocalMap

#

ThreadLocalMap

static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value; // 强引用!

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}

private Entry[] table;
}

关键设计

  • Key 是弱引用ThreadLocal 对象可以被 GC 回收
  • Value 是强引用:即使 Key 被回收,Value 仍然存在

内存泄漏原因

#

泄漏场景

// 线程池环境
ExecutorService pool = Executors.newFixedThreadPool(10);

pool.submit(() -> {
ThreadLocal<byte[]> local = new ThreadLocal<>();
local.set(new byte[1024 * 1024 * 100]); // 100MB
// 业务逻辑...
// 忘记 remove()!
});

问题

  1. ThreadLocal 引用被释放(方法结束,局部变量)
  2. 但线程池中的线程不会销毁
  3. ThreadLocalMap 中的 Entry:Key 为 null(弱引用被回收),Value 仍为 100MB
  4. 线程一直存活,Value 一直无法释放

#

图解

Thread(线程池线程,长期存活)
└── ThreadLocalMap
├── Entry[0]: Key=null(弱引用已回收), Value=100MB ← 泄漏!
├── Entry[1]: Key=ThreadLocal@xxx, Value=...
└── ...

解决方案

#

1. 及时 remove()

try {
threadLocal.set(value);
// 业务逻辑
} finally {
threadLocal.remove(); // 必须清理
}

#

2. 使用 try-with-resources 模式

public class AutoCloseableThreadLocal<T> extends ThreadLocal<T> 
implements AutoCloseable {

@Override
public void close() {
this.remove();
}
}

// 使用
try (AutoCloseableThreadLocal<User> tl = new AutoCloseableThreadLocal<>()) {
tl.set(new User());
// 自动调用 remove()
}

#

3. 使用 InheritableThreadLocal(父子线程传递)

// 子线程继承父线程的 ThreadLocal 值
InheritableThreadLocal<String> inheritable = new InheritableThreadLocal<>();
inheritable.set("父线程的值");

new Thread(() -> {
System.out.println(inheritable.get()); // "父线程的值"
}).start();

注意:线程池中使用 InheritableThreadLocal 需要配合 TransmittableThreadLocal(阿里开源)。

ThreadLocalMap 的清理机制

#

线性探测 + 清理

private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;

// 清除当前slot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;

// 重新哈希后续entry
// ...
}

#

触发时机

  1. set() 时:发现 Key 为 null 的 Entry,清理
  2. get() 时:探测过程中清理遇到的过期 Entry
  3. remove() 时:主动清理
  4. 扩容时:全表扫描清理

问题:只有当再次访问 ThreadLocal 时才会清理,如果一直不访问,泄漏的 Value 一直存在。

最佳实践

#

1. 始终在使用后 remove()

public void process() {
context.set(user);
try {
doWork();
} finally {
context.remove(); // 确保清理
}
}

#

2. 使用静态 ThreadLocal

// 推荐
private static final ThreadLocal<User> context = new ThreadLocal<>();

// 不推荐:每个实例创建一个
private final ThreadLocal<User> context = new ThreadLocal<>();

#

3. 注意线程池环境

// Spring 的 @Async 线程池
@Async("taskExecutor")
public void asyncTask() {
try {
User user = UserContext.get();
// ...
} finally {
UserContext.remove(); // 必须清理
}
}

#

4. 考虑使用替代方案

场景 替代方案
请求上下文 方法参数传递
日期格式化 DateTimeFormatter
随机数 ThreadLocalRandom

总结

要点 说明
原理 Thread 内部维护 ThreadLocalMap
Key 弱引用,可被 GC
Value 强引用,是泄漏根源
预防 使用完必须 remove()
高危场景 线程池 + 大对象

ThreadLocal 是强大的工具,但在线程池环境下必须谨慎使用。记住:每次 set() 后,都要有对应的 remove()

核心要点

  1. ThreadLocal 为每个线程提供独立的变量副本

  2. 底层使用 ThreadLocalMap 存储,key 是弱引用

  3. 内存泄漏的原因:ThreadLocal 被回收,但 Entry 仍然引用着 value

  4. 解决方案:使用完毕后调用 remove() 方法

总结

ThreadLocal 是一个强大的工具,但需要谨慎使用。在实际项目中,结合 try-finally 块确保资源被正确清理,可以有效避免内存泄漏问题。


   转载规则


《ThreadLocal的使用和内存泄漏》 小乐 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录