线程重用导致ThreadLocal的数据混乱问题

使用spring框架开发业务代码的时候,会把业务处理Service类交给spring ioc管理,最终它会是一个单例bean,为了避免线程安全问题,则不好在这些类中定义成员变量,于是我们会想到使用 ThreadLocal 包裹成一个变量。ThreadLocal适用于变量在线程间隔离,而在方法或类间共享的场景。如果用户信息获取比较昂贵(例如需要调接口从其它微服务中查询),那么在 ThreadLocal中缓存数据是比较合适的做法,但,这么做有可能会出现用户信息错乱的Bug。可以通过如下的方式复现:

使用springboot快速构建一个web应用程序,创建OrderController

@RestController
public class OrderController {

    final ThreadLocal<String> userThreadLocal = ThreadLocal.withInitial(() -> null);

    @Autowired(required = false)
    private OrderService orderService;

    @GetMapping("/order/wrong")
    public String createOrder(OrderCreateRequest request) {
        // 设置用户信息之前先查一次
        final String currentUser = userThreadLocal.get();
        System.out.println("threadLocal中的用户为:" + currentUser);
        // 使用userService查询当前用户信息并缓存到userThreadLocal中
        String user = "user from userService";
        userThreadLocal.set(user);
        System.out.println(Thread.currentThread().getName() + "中用户信息为:" + userThreadLocal.get());
        return user;
    }


}

按理说,在查询用户之前第一次获取的值始终应该为null,但是在测试中发现,偶尔有几次不是null。为了方便发现这个问题,我们在配置文件中设置一下tomcat的线程数:

server:
    tomcat:
        threads:
            max: 1

测试结果如下:
在这里插入图片描述

仅第一次线程中的用户信息为 null,后面每次新请求过来,线程ThreadLocal中似乎已经有值了。

原因大概大家已经知道了,程序运行在tomcat中,执行程序的线程是tomcat的工作线程,而tomcat的工作线程是基于线程池的。线程池会重用固定的几个线程,我们将 server.tomcat.threads.max 设置为1,把工作线程池最大线程数设置为1,这样始终是同一个线程在处理请求,因此下一个请求的线程中会遗留上一次请求的信息。

解决方案只需要在每次请求结束之后将ThreadLocal清空或者在请求之前将其清空即可。

@GetMapping("/order/wrong")
public String createOrder(OrderCreateRequest request) {
    try {
        // 设置用户信息之前先查一次
        final String currentUser = userThreadLocal.get();
        System.out.println("threadLocal中的用户为:" + currentUser);
        // 使用userService查询当前用户信息并缓存到userThreadLocal中
        String user = "user from userService";
        userThreadLocal.set(user);
        System.out.println(Thread.currentThread().getName() + "中用户信息为:" + userThreadLocal.get());
        return user;
    } finally {
        userThreadLocal.remove();
    }
}

在这里插入图片描述


版权声明:本文为jisuanji12306原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。