背景
我们都知道ThreadLocal实现了资源在线程内独享,线程之间隔离。
实际使用中,ThreadLocal适用于变量在线程间隔离,而在方法或类间共享的场景。比如用户信息,当用户信息需要在多个方法之间传递或者共享使用的时候,同时,每个Tomcat请求的用户信息是私有的。这时可使用ThreadLocal,即直接从线程的ThreadLocal中获取用户信息,而不用在方法的形参中获取。
但是,不当的使用也会带来一些问题,比如在线程池中使用ThreadLocal可能会导致获取到ThreadLocal中的历史数据,造成数据错乱。如Tomcat中使用的多线程是线程池实现的,如果不当使用ThreadLocal, 由于线程重用,就会有该数据错乱问题。本篇博文会举例说明线程重用时,ThreadLocal变量会造成什么影响,并给出合适的解决方案。
案例说明
使用 Spring Boot 创建一个 Web 应用程序,使用 ThreadLocal 存放一个 Integer 的值,来暂且代表需要在线程中保存的用户信息,这个值初始是 null。
在业务逻辑中,我先从 ThreadLocal 获取一次值,然后把外部传入的参数设置到 ThreadLocal 中,来模拟从当前上下文获取到用户信息的逻辑,随后再获取一次值,最后输出两次获得的值和线程名称。
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;import java.util.HashMap;
import java.util.Map;@RestController
@RequestMapping(value = "threadlocal")
public class ThreadLocalTestClass {private ThreadLocal<Integer> currentUser = ThreadLocal.withInitial(() -> null);@GetMapping(value = "wrong")public Map wrong(@RequestParam("id") 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; }
}
执行结果
图1
图2
图2的正确结果应该是 null, 2。但是此时却访问到了请求1的用户信息。并未实现请求1和请求2的隔离性!
执行结果说明
* 以上方法中我们将tomcat的最大线程数设置为1。故只有一个线程在工作,所以线程会重用。* 由于我们使用了ThreadLocal, 每次请求完以后,线程中存放的ThreadLocal设置的值(本例是用户信息)依然存在。* 所以就可能导致另一个用户的请求打进来以后,从ThreadLocal中获取到的用户信息是上一个用户的信息。* 解决办法比较简单,就是在代码的 finally 代码块中,显式清除 ThreadLocal中的数据。* 这样一来,新的请求过来即使使用了之前的线程也不会获取到错误的用户信息了。
配置文件
正确的代码写法
@GetMapping(value = "right")public Map wrong(@RequestParam("id") Integer userId) {// 设置用户信息之前先查询一次ThreadLocal中的用户信息String before = Thread.currentThread().getName() + ":" + currentUser.get();// 设置用户信息到ThreadLocalcurrentUser.set(userId);// 设置用户信息之后,再查询一次ThreadLocal中的用户信息try {String after = Thread.currentThread().getName() + ":" + currentUser.get();// 汇总输出两次查询结果Map result = new HashMap();result.put("before", before);result.put("after", after);return result;} finally {// 返回设置的新值以后,将Threadlocal中的用户信息清除,防止被下一个请求访问到,造成脏数据。currentUser.remove();}}
正确的结果
这样的话,即使线程重用了,每个Tomcat请求也不会访问到其他请求设置的值了,就不会出现数据错乱问题。