线程池中线程重用导致的问题
之前在公司做的一个项目中,有一个 core 的公共依赖包,那个依赖里面简单封装了用户的信息。
突然有一天在生产上遇到一个奇怪的问题,有时获取到的用户信息是别人的。我就下把 core 包代码下载下来,查看代码后,我发现使用了 ThreadLocal 来缓存获取到的用户信息。
ThreadLocal 是用于变量在线程间隔离,前端通过加密的 token ,core 包把 token 解密成用户信息放在 ThreadLocal 中也没什么问题的。但,这么做为什么会出现用户信息错乱的 Bug 呢?
自己写个 demo 试试
Spring Boot 创建一个 Web 应用程序,使用 ThreadLocal 存放一个测试值,代表需要在线程中保存的用户信息,这个值初始是 null。在业务逻辑中,我先从 ThreadLocal 获取一次值,然后把外部传入的参数设置到 ThreadLocal 中,来模拟从当前上下文获取到用户信息的逻辑,随后再获取一次值,最后输出两次获得的值和线程名称。
@GetMapping("/wrong/{token}") public Map<String, String> userInfo(@PathVariable String token) { ThreadLocal<String> userinfo = new ThreadLocal<>(); Map<String, String> map = new HashMap<>(); map.put("before", userinfo.get()); userinfo.set(token); map.put("after", userinfo.get()); map.put("threadName", Thread.currentThread().getName()); return map; }
我们执行一下,看看结果
从结果中我们可以看到,我们请求了两次接口,token 分别是 user1 和 user2 , 返回换结果是没有任何问题的, before 的值为 null, after 的值为当前用户的 token 。但是我们知道, tomcat 容器用的也是线程池来处理这些请求的, 如果第一个请求和第二个请求用的是同一个线程会出现什么情况呢。为了出现这种情况,我们修改一下 tomcat 的配置:
server.tomcat.max-threads=1
重新请求一下,看结果
从上面我们可以看到第一次请求是没有什么问题,问题就在于同一个线程在第二次请求的时候是存着user1的信息的。
这个demo是比较极端的,主要是告诉大家线程池会重用固定的几个线程,一旦线程重用,那么很可能首次从 ThreadLocal 获取的值是之前其他用户的请求遗留的值。这时,ThreadLocal 中的用户信息就是其他用户的信息。
所以通过这个示例告诉大家,我们在写业务代码时,第一点就是要理解代码会跑在什么线程上:我们可能会抱怨学多线程没用,因为代码里没有开启使用多线程。但可能只是我们没有意识到,在 Tomcat 这种 Web 服务器下跑的业务代码,本来就运行在一个多线程环境(否则接口也不可能支持这么高的并发),并不能认为没有显式开启多线程就不会有线程安全问题。存在线程池的地方就意味着线程会被重用。使用 ThreadLocal 这种和线程强绑定的工具来存放一些数据时,需要特别注意在代码运行完后,显式地去清空设置的数据。如果在代码中使用了自定义的线程池,也同样会遇到这个问题。
下面我们就根据上面的原则来改造一下代码,让它能够按照我们的预期运行:在最后的地方我们加入代码来显式地清除 ThreadLocal 里面的东西
@GetMapping("/right/{token}") public Map<String, String> userInfo(@PathVariable String token) { Map<String, String> map = new HashMap<>(); map.put("before", userinfo.get()); userinfo.set(token); try { map.put("after", userinfo.get()); map.put("threadName", Thread.currentThread().getName()); return map; } finally { userinfo.remove(); } }
再次请求一下看看效果
这样就不会出现在此线程中存在着上次请求的数据。