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; } }
|
#
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]); });
|
问题:
ThreadLocal 引用被释放(方法结束,局部变量)
- 但线程池中的线程不会销毁
ThreadLocalMap 中的 Entry:Key 为 null(弱引用被回收),Value 仍为 100MB
- 线程一直存活,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()); }
|
#
3. 使用 InheritableThreadLocal(父子线程传递)
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; tab[staleSlot].value = null; tab[staleSlot] = null; size--; }
|
#
触发时机
- set() 时:发现 Key 为 null 的 Entry,清理
- get() 时:探测过程中清理遇到的过期 Entry
- remove() 时:主动清理
- 扩容时:全表扫描清理
问题:只有当再次访问 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. 注意线程池环境
@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()。
核心要点
ThreadLocal 为每个线程提供独立的变量副本
底层使用 ThreadLocalMap 存储,key 是弱引用
内存泄漏的原因:ThreadLocal 被回收,但 Entry 仍然引用着 value
解决方案:使用完毕后调用 remove() 方法
总结
ThreadLocal 是一个强大的工具,但需要谨慎使用。在实际项目中,结合 try-finally 块确保资源被正确清理,可以有效避免内存泄漏问题。