1.没有意识到线程重用导致用户信息错乱的 Bug
ThreadLocal:
首先我们先了解一下这个类:
threadlocal是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据
/**
* This class provides thread-local variables. These variables differ from
* their normal counterparts in that each thread that accesses one (via its
* {@code get} or {@code set} method) has its own, independently initialized
* copy of the variable. {@code ThreadLocal} instances are typically private
* static fields in classes that wish to associate state with a thread (e.g.,
* a user ID or Transaction ID).
*/
解释一下,就是说:
/**
*大致意思就是ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应
*的互相独立的。通过get和set方法就可以得到当前线程对应的值。
*/
通过上面的解释,我们可以猜想到它的对应工作场景:
适用于变量在线程间隔离,而在方法或类间共享的场景。如果用户信息的获取比较昂贵(比如从数据库查询用户信息),那么在 ThreadLocal 中缓存数据是比较合适的做法
但在获取的时候我们偶尔会出现一个情况就是---在Spring Boot的框架下,在Tomcat里,有时获取到的用户信息是别人的。
这个问题的解答其实很好理解:
那么我们可以先了解到:Tomcat的工作线程是基于线程池的
这样的话我么们就可以推论出:“线程池会重用固定的几个线程,一旦线程重用,那么很可能首次从 ThreadLocal 获取的值是之前其他用户的请求遗留的值。这时,ThreadLocal 中的用户信息就是其他用户的信息。”--------极客时间《Java 业务开发常见错误 100 例》
下面给一个测试用例:
private static final ThreadLocal<Integer> currentUser = ThreadLocal.withInitial(() -> null);
@GetMapping("wrong")
public Map wrong(@RequestParam("userId") Integer userId) {
//设置用户信息之前先查询一次ThreadLocal中的用户信息
String before = Thread.currentThread().getName() + ":" + currentUser.get();
//设置用户信息到ThreadLocal
currentUser.set(userId);
//设置用户信息之后再查询一次ThreadLocal中的用户信息
String after = Thread.currentThread().getName() + ":" + currentUser.get();
//汇总输出两次查询结果
Map result = new HashMap();
result.put("before", before);
result.put("after", after);
return result;
}
然后,我们把Tomcat的参数也修改一下(配置文件中进行),将线程数设置为1,这样能更快重现一下场景
server.tomcat.max-threads=1
然后分别用两个用户去请求,第一个会显然是没有问题的,而第二个就会获取到第一个用户的信息,这个就是Tomcat 的线程池重用了线程所导致的。
而如何去解决这个问题呢,我们只需要显性的去清理一下数据,具体代码修改如下,你可以进行对比验证:
@GetMapping("right")
public Map right(@RequestParam("userId") Integer userId) {
String before = Thread.currentThread().getName() + ":" + currentUser.get();
currentUser.set(userId);
try {
String after = Thread.currentThread().getName() + ":" + currentUser.get();
Map result = new HashMap();
result.put("before", before);
result.put("after", after);
return result;
} finally {
//在finally代码块中删除ThreadLocal中的数据,确保数据不串
currentUser.remove();
}
}
从这一个我们可以了解到两点:
1.不能认为没有显式开启多线程就不会有线程安全问题
2.使用类似 ThreadLocal 工具来存放一些数据时,需要特别注意在代码运行完后,显式地去清空设置的数据。
同样的,ThreadLocal 是利用独占资源的方式,来解决线程安全问题,那如果我们确实需要有资源在线程之间共享,我们可能就需要用到线程安全的容器,那就是下面的话题了
使用了线程安全的并发工具,并不代表解决了所有线程安全问题
这个就交到下一篇来更新吧!!
具体请关注微信公众号月夜Moonlight,文章链接:https://mp.weixin.qq.com/s/Db1HYvhXOnjvWMC2FkPeSw