在SpringBoot Web项目中使用ThreadLocal变量记录用户信息,踩坑记录。
一、简介ThreadLocal变量
简单描述ThreadLocal变量:ThreadLocal变量是属于线程独有的遍历,不同线程之间相互隔离,每个线程只可访问属于自己线程的ThreaLocal变量。
简单使用:
public static ThreadLocal<String> userName = new ThreadLocal<>();
//赋值
userName.set("张三");
//取值
String name = userName.get();
//移除
userName.remove();
核心源码:
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//可以简单理解为线程的一个Map属性
ThreadLocalMap map = getMap(t);
if (map != null)
//map中put值,key 为ThreadLocal对象
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取线程对应ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//值null
return setInitialValue();
}
public void remove() {
//根据线程获取对应的ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
//从ThreadLocalMap中移除对应entry
m.remove(this);
}
简单概述下核心点:
1、每个线程都有一个ThreadLocalMap对象(不深入可以就当成一个普通map)
2、ThreadLocalMap中存储ThreadLocal相关的遍历和值,存储方式ThreadLocal为key,值为value的键值对
3、ThreadLocalMap变量的生命周期与线程生命周期相关
二、SpringBoot下踩坑分析
项目简介:项目为使用springboot的web项目,使用内置tomcat。用户信息存储在redis中使用token为key,同时会使用ThreadLocal变量保存获取到的用户信息。获取用户信息时先从ThreadLocal获取,获取不到再从redis获取,然后写入ThreadLocal。
出现问题:多用户并发访问时,出现权限混乱,B用户获取到A用户的用户信息
问题原因:从ThreaLocal变量中拿到的用户信息出现错误,根本原因与线程池有关,Springboot中内嵌Tomcat处理请求使用的线程池,即从线程池中获取空闲线程使用,使用结束的线程并不会销毁(默认初始化工作线程10),因为在使用ThreadLocal时没有执行remove方法,所以线程的ThreadLocalMap对象初始化后就始终存在,后续再从线程池中拿到之前使用过的线程后,其中值依然存在,从而导致错误。
处理方法:线程结束后对ThreadLocal变量执行remove方法,springboot web项目可以使用拦截器处理。
如下是对出错部分的模拟测试:
public class TestThreadLocal {
public static ThreadLocal<String> userName = new ThreadLocal<>();
public static AtomicInteger count = new AtomicInteger();
@GetMapping("test")
public String getThreadInfo(){
int num = count.incrementAndGet();
String currentThreadName = Thread.currentThread().getName() +"==========="+ num;
String localThreadName = this.getLocalThreadInfo(currentThreadName);
return String.format("当前线程:%s, ThreadLocal:%s",currentThreadName, localThreadName);
}
private String getLocalThreadInfo(String name){
String threadName = userName.get();
if(StringUtils.hasText(threadName)){
log.info("当前线程:{},ThreadLocal:{}",name,threadName);
}else{
threadName = name;
log.info("当前线程:{},时间:{}", threadName, System.currentTimeMillis());
userName.set(threadName);
}
return threadName;
}
}
//输出
当前线程:http-nio-8080-exec-1===========1,时间:1688627451142
当前线程:http-nio-8080-exec-2===========2,时间:1688627451603
当前线程:http-nio-8080-exec-3===========3,时间:1688627452039
当前线程:http-nio-8080-exec-4===========4,时间:1688627452335
当前线程:http-nio-8080-exec-5===========5,时间:1688627452648
当前线程:http-nio-8080-exec-6===========6,时间:1688627452931
当前线程:http-nio-8080-exec-7===========7,时间:1688627453249
当前线程:http-nio-8080-exec-8===========8,时间:1688627453649
当前线程:http-nio-8080-exec-9===========9,时间:1688627454380
当前线程:http-nio-8080-exec-10===========10,时间:1688627455357
当前线程:http-nio-8080-exec-1===========11,ThreadLocal:http-nio-8080-exec-1===========1
当前线程:http-nio-8080-exec-2===========12,ThreadLocal:http-nio-8080-exec-2===========2