01 引言
在Java多线程编程中,ThreadLocal
是一个强大却常被误解的工具。它能为每个线程创建变量的独立副本,避免线程间的数据干扰,堪称线程安全的“变量保险箱”。但若使用不当,它也可能成为内存泄露的隐形杀手。
之前在父子线程提到过ThreadLocal
,本章将详细介绍其原理,使用以及如何避免被面试官长问的内存泄露问题。
02 实现原理
ThreadLocal
用来保存线程的独有变量,保存在其内部的一个内部类(ThreadLocalMap
)中,也是一种k-v
结构。k
就是当前的ThreadLocal
对象,而v
就是我们想要保存的值。
那现在有三个对象出现在我们面前:Thread
、ThreadLocal
和ThreadLocalMap
他们之间是怎样的关系呢?我们先看看Thread
类中的关键属性:
可以看出Thread
中维护了两个ThreadLocal.ThreadLocalMap
①②,其中②是被InheritableThreadLocal
类维护的,专门为父子线程继承关系设计的,又继承ThreadLocal
,所以和ThreadLocal
的功能一致,本节不做不讨论。
而①正是被ThreadLocal
维护的属性,所以我们就可以看到这样的关系:Thread
维护了ThreadLocalMap
成员变量,而ThreadLocalMap
维护了一个弱引用的Entry
内部类,而Entry
则以ThreadLocal
作为key
,维护着我们需要需要保存的value
。大致如图:
而ThreadLocalMap
的构造类似一个HashMap
的结构,可以直接看做一个Map
。
03 使用场景
ThreadLocal
提供了一种线程级别的数据存储机制,每一个线程都有自己独立的ThreadLocal
副本,每个线程就可以独立的操作这些数据信息,并不会对其他线程产生影响。
3.1 会话管理
很多应用中需要用户登录鉴权,校验通过之后就会把用户登录的信息存储在ThreadLocal
中,这样后续的流程就可以直接从ThreadLocal
中获取就好了。
public class UserContextHolder {
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(); // 关键清理!
}
}
类似上面的代码一般会作为一个工具类,用户登录成功之后会调用set()
方法保存用户信息,获取时就是调用get()
方法。直到当前线程结束,则需要调用clear()
方法清理数据。
登录通过时,通常在拦截器里面被调用:
public class LoginInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 登录校验......
User user = ...
// 保存登录信息
UserContextHolder.set(user);
return super.preHandle(request, response, handler);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 调用完成之后清理数据
UserContextHolder.clear();
}
}
3.2 线程安全
ThreadLocal
本身就是线程安全的,可以用来存储一个线程不安全的类,来保证其线程安全。如SimpleDateFormat
。
private static ThreadLocal<SimpleDateFormat> dateFormat =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
// 每个线程获取自己的SimpleDateFormat实例,避免多线程竞争
String date = dateFormat.get().format(new Date());
3.3 数据库事务管理
Spring
框架使用ThreadLocal
保存当前线程的事务连接,确保同一事务中的所有操作使用同一个Connection
,包括多数据源的切换。
3.4 链路追踪TraceId
traceId
经常被用在链路追踪的日志中,金记录每次请求的未必表示,通常也被记录在ThreadLocal
中。
04 内存泄露
ThreadLocal
的内存泄露经常被面试官问到,算是一道经典的面试题。ThreadLocal
已经尽可能的帮我们解决了一部分问题,比如引入弱引用。但是依然还有有内存泄露的风险,需要我们开着自己解决。
解决的方式也很简单,直接使用ThreadLocal
中的remove()
方法即可。
4.1 内存泄露发生的过程
导致内存泄露部分就是其在堆上存储的ThreadLocalMap
中的K-V
部分。
- 当
ThreadLocal
对象失去强引用(如设置为null
)时,由于Key是
弱引用(继承WeakReference<ThreadLocal<?>>
),下次GC
时Key
会被回收,Entry
变为Key=null, Value=原值
。 - 但
Value
仍被Entry
强引用,而Entry
又被ThreadLocalMap
强引用,Map
又被Thread
(常驻线程池)强引用。 - 若未调用
remove()
,Value
会一直占用内存,导致泄露。
4.2 线程池加剧泄露
线程池的创建一般都是指定核心线程,而核心线程不会不会随着任务的结束而结束,ThreadLocal
也不会被销毁。再加上其他线程的ThreadLocal
,更容易造成内存的泄露。
05 小结
ThreadLocal
如一把双刃剑:合理使用可优雅解决线程数据隔离问题,疏于管理则会导致内存泄露。牢记 “用后即焚” 原则,在finally中调用remove()
,方能发挥其威力而不被反噬。