做网站外包的公司好干嘛,网站子页怎么做 视频,为什么手机进网站乱码,东莞营销推广在生产上遇到一个诡异的问题#xff0c;有时获取到的用户信息是别人的。查看代码后#xff0c;我发现他使用了 ThreadLocal 来缓存获取到的用户信息。
我们知道#xff0c;ThreadLocal 适用于变量在线程间隔离#xff0c;而在方法或类间共享的场景。如果用户信息的获取比较…在生产上遇到一个诡异的问题有时获取到的用户信息是别人的。查看代码后我发现他使用了 ThreadLocal 来缓存获取到的用户信息。
我们知道ThreadLocal 适用于变量在线程间隔离而在方法或类间共享的场景。如果用户信息的获取比较昂贵比如从数据库查询用户信息那么在 ThreadLocal 中缓存数据是比较合适的做法。但这么做为什么会出现用户信息错乱的 Bug 呢
我们看一个具体的案例吧。
使用 Spring Boot 创建一个 Web 应用程序使用 ThreadLocal 存放一个 Integer 的值来暂且代表需要在线程中保存的用户信息这个值初始是 null。在业务逻辑中我先从 ThreadLocal 获取一次值然后把外部传入的参数设置到 ThreadLocal 中来模拟从当前上下文获取到用户信息的逻辑随后再获取一次值最后输出两次获得的值和线程名称。
private static final ThreadLocalInteger currentUser ThreadLocal.withInitial(() - null);GetMapping(wrong)
public Map wrong(RequestParam(userId) Integer userId) {//设置用户信息之前先查询一次ThreadLocal中的用户信息String before Thread.currentThread().getName() : currentUser.get();//设置用户信息到ThreadLocalcurrentUser.set(userId);//设置用户信息之后再查询一次ThreadLocal中的用户信息String after Thread.currentThread().getName() : currentUser.get();//汇总输出两次查询结果Map result new HashMap();result.put(before, before);result.put(after, after);return result;
}按理说在设置用户信息之前第一次获取的值始终应该是 null但我们要意识到程序运行在 Tomcat 中执行程序的线程是 Tomcat 的工作线程而 Tomcat 的工作线程是基于线程池的。
顾名思义线程池会重用固定的几个线程一旦线程重用那么很可能首次从 ThreadLocal 获取的值是之前其他用户的请求遗留的值。这时ThreadLocal 中的用户信息就是其他用户的信息。
为了更快地重现这个问题我在配置文件中设置一下 Tomcat 的参数把工作线程池最大线程数设置为 1这样始终是同一个线程在处理请求 server.tomcat.max-threads1 运行程序后先让用户 1 来请求接口可以看到第一和第二次获取到用户 ID 分别是 null 和 1符合预期 随后用户 2 来请求接口这次就出现了 Bug第一和第二次获取到用户 ID 分别是 1 和 2显然第一次获取到了用户 1 的信息原因就是 Tomcat 的线程池重用了线程。从图中可以看到两次请求的线程都是同一个线程http-nio-8080-exec-1。 这个例子告诉我们在写业务代码时首先要理解代码会跑在什么线程上
我们可能会抱怨学多线程没用因为代码里没有开启使用多线程。但其实可能只是我们没有意识到在 Tomcat 这种 Web 服务器下跑的业务代码本来就运行在一个多线程环境否则接口也不可能支持这么高的并发并不能认为没有显式开启多线程就不会有线程安全问题。 因为线程的创建比较昂贵所以 Web 服务器往往会使用线程池来处理请求这就意味着线程会被重用。这时使用类似 ThreadLocal 工具来存放一些数据时需要特别注意在代码运行完后显式地去清空设置的数据。如果在代码中使用了自定义的线程池也同样会遇到这个问题。
理解了这个知识点后我们修正这段代码的方案是在代码的 finally 代码块中显式清除 ThreadLocal 中的数据。这样一来新的请求过来即使使用了之前的线程也不会获取到错误的用户信息了。修正后的代码如下
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();}
}
重新运行程序可以验证再也不会出现第一次查询用户信息查询到之前用户请求的 Bug 总结: 使用 ThreadLocal 来缓存数据以为 ThreadLocal 在线程之间做了隔离不会有线程安全问题没想到线程重用导致数据串了。请务必记得在业务逻辑结束之前清理 ThreadLocal 中的数据。