简介
ThreadLocal是Java标准库中提供的,用于在多线程环境中,为每个线程创建相互隔离的变量副本的类。通过线程隔离的方式防止共享资源所产生的线程安全问题。并且线程可以在代码执行的任意地点拿到存储在ThreadLocal中的变量。
原理
源码分析
观察ThreadLocal类源码我们可以发现,其中并没有关于存入的成员变量的定义,在对存入元素执行get和set操作时,读写的并非是ThreadLocal内部的成员变量的值,而是通过执行相应业务逻辑从Thread线程类中读写的。让我们观察ThreadLocal的Set方法。
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
首先获取了当前线程t,然后以t为参数获取了线程的成员ThreadLocalMap,这个变量是一个以ThreadLocal<>为key,以待存入值为value的Map。通过当前threadLocal,即可访问到value。
ThreadLocalMap是Thread的成员变量,而ThreadLocal是用来操作的窗口。
因此,多线程情况下,每个线程使用同一个ThreadLocal变量进行共享资源操作而不会导致线程安全的原因,就在于ThreadLocal将共享资源放在了各个线程的内部。
流程图
用法
web用户身份信息存储
在web系统当中,处理一次请求往往需要跟踪用户的会话信息,例如在业务层中拿到当前登录用户的id等信息。但整个流程的执行是分散的(存在拦截器、过滤器、aop、鉴权框架、控制层),彼此之间难以共享信息。但由于这些流程都是由一个线程处理的,因此可以在首次获取用户信息时,将用户信息存储在ThreadLocal中,由于前面提到的原理,可以在另一处拿到属于该线程的,相同的用户信息。
public class LoginContext {
private static final ThreadLocal<UserDO> loginUser = new ThreadLocal<>();
public static void setLoginUser(UserDO user) {
loginUser.set(user);
}
public static UserDO getLoginUser() {
return loginUser.get();
}
}
// token拦截器等
user = token.getUser();
LoginContext.setLoginUser(user); // 设置登录上下文
// Controller
@GetMapping("/info")
public UserDO user() {
UserDO user = LoginContext.getLoginUser();
System.out.println("当前登录用户:"+user);
return user;
}
日志记录
以线程(web中的单次请求)为单位进行日志记录,同时不需要进行参数的传递,可以在任意代码执行地点拿到日志上下文并进行记录
简化参数传递
在调用层次较深,参数传递较为复杂的情况下,可以在不同业务层中通过ThreadLocal进行数据传输,以达到无感传递复杂参数的目的。
潜在问题
内存泄漏
通过ThreadLocal的原理,我们可以知道其所记录的变量是和线程高度绑定的。这在使用线程池的情况下,较容易产生内存泄漏的问题。一是新的任务可能会读取到旧任务在ThreaLocal中的残留值,导致数据安全问题。二是ThreadLocalMap中的value可能会泄漏,导致内存溢出的风险。
关于value的内存泄漏,我们都知道ThreadLocalMap的key是弱引用ThreadLocal的,也就意味着如果在线程执行过程中,ThreadLocal在外部解除强引用后,ThreadLocalMap的key由于弱引用,key实例就会随之被回收变为null。但是value还被强引用导致无法回收,因此产生了泄漏。
解决这一问题的方法,通常是在使用完threadLocal之后,调用其remove()方法,清除掉ThreadLocalMap中的entry。并且将threadLocal定义为static final,保证其强引用的存在,避免提前被回收掉。