前言:
ThreadLocal 可以保证多线程(高并发)的安全,下面以一个生产遇到的问题来进一步描述。
1. 问题回顾
下面是一个 util 类,这个类的 callUser 变量是全局公有的,在 SpringBoot 工程的拦截器中获取调用者信息(即 callUser)并填充到 ContextUtil 的 callUser 变量中,后续打印日志或者记录调用信息时直接用 getCallUser() 获取。
public class ContextUtil {
private static String callUser;
public static String getCallUser() {
return callUser;
}
public static void setCallUser(String callUser) {
ContextUtil.callUser = callUser;
}
}
上述功能很简单,最开始使用的时候也没啥问题,可是后来推广使用中发现没有权限调用某接口的人居然在数据库有调用的记录,并且对方反馈没调用过(后来测试发现,对方账号即使调用也会报权限错误,不会在数据库生成调用记录)。
2. 问题梳理
上述问题出现后,逐步开始排查,排查问题在测试环境中进行,故障无法复现;在生产环境错过高峰时段排查,故障依然无法复现;因为暂时无法复现故障,并且不影响使用,所以就搁置了,搁置前多加了一些日志,并且开始记录故障发生的时间和频率。
一周后,复盘该问题:故障记录显示,工作日下午 3:00 -- 4:00 是故障频发的时间端(高峰使用时段),其它时间没有发生故障。那就基本可以确定了,程序中存在线程安全的问题,并且错误字段也清楚,所以也就基本可以找到相关代码,也就是上面那段,看到代码真容的时候,也就清楚问题所在了。
3.问题处理
以上问题其实是一个比较初级的问题,callUser 是全局公有变量,任何线程都可以使用,那么问题来了,假如两个线程几乎同时运行,callUser 不一致,先赋值的线程必然会被后赋值的线程覆盖掉 callUser。举个例子模拟一下:
public class Main {
public static void main(String[] args) {
String firstCallUser = "firstCallUser";
String secondCallUser = "secondCallUser";
Thread t1 = new Thread() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
ContextUtil.setCallUser(firstCallUser);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(firstCallUser + " is " + ContextUtil.getCallUser());
}
};
Thread t2 = new Thread() {
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
ContextUtil.setCallUser(secondCallUser);
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(secondCallUser + " is " + ContextUtil.getCallUser());
}
};
t1.start();
t2.start();
}
}
t1 和 t2 线程持续的时间几乎一致(2000ms),t2 线程启动 500ms 后赋值 callUser,t1 线程启动 1000ms 赋值 callUser(覆盖了 t1 线程),那么 t2 在获取 callUser 时就会获取 t1 的赋值,程序运行结果也证实了上述推论:
问题处理也比较简单,callUser 使用 ThreadLocal 声明,如下:
public class ContextUtil {
private final static ThreadLocal<String> callUser = new ThreadLocal<>();
public static String getCallUser() {
return callUser.get();
}
public static void setCallUser(String call) {
callUser.set(call);
}
}
再次执行上述测试方法,结果正常: