ThreadLocal是Java中的一个重要的类,其提供了一种创建线程局部变量机制。从而使得每个线程都有自己独立的副本,互不影响。此外,ThreadLocal也是面试的一个重点,对于此网上已经有很多经典文章来进行分析,但今天我们主要分析笔者在项目中遇到的一个错误使用ThreadLocal的示例,并针对错误原因进行深入剖析,理论结合实践让你更加透彻的理解ThreadLocal的使用。
Java中的ThreadLocal是一种用于在多线程环境中存储线程局部变量的机制,它为每个线程都提供了独立的变量副本,从而避免了线程之间的竞争条件。事实上,ThreadLocal的工作原理是在每个线程中创建一个独立的变量副本,并且每个线程只能访问自己的副本。
进一步,ThreaLocal可以在当前线程中独立的保存信息,这样就方便同一个线程的其他方法获取到该信息。 因此,ThreaLocal的一个最广泛的使用场景就是将信息保存,从而方便后续方法直接从线程中获取。
@RestController @Slf4j @RequestMapping("/user") public class UserController { @Autowire private UserService userService; @GetMapping("get-userdata-byId") public CommonResult<Object> getUserData(Integer uid) { return userService.getUserInfoById(uid); }服务层
@Service public class UserService { ThreadLocal<UserInfo> locals = new ThreadLocal<>(); public CommonResult<UserInfo> getUserInfoById ( String uid) { UserInfo info = locals.get(); // 堆代码 duidaima.com if (info == null) { // 调用uid查询用户 UserInfo userInfo = UserMapper.queryUserInfoById(uid); locals.set(userInfo); } // ....省略后续会利用UserInfo完成某些操作 return CommonResult.success(info); } }(注:此处为了方便复现项目代码进行了简化,重点在于理解ThreaLocal的使用)
先来简单介绍一下业务逻辑,前台通过url访问/user/get-userdata-byId后,后端会根据传入的uid信息查询用户信息,以避免进而根据用户信息执行相应的处理逻辑。进一步,在服务层中会缓存当前id对应的用户信息,避免频繁的查询数据库。
直观来看,上述代码似乎没问题。但最近用户反馈会出现这样一个问题,就是用户A登录系统后,查询到的可能是用户B的信息,这个问题就很诡异了。遇到问题不要慌,不妨来看看笔者是如何进行思考,来定位,解决问题的。
首先,用户A登录系统后,前端访问/user/get-userdata-byId时携带的uid信息肯定是用户A的uid信息;进一步,传到控制层getUserData处的uid信息肯定是用户A的uid。所以,发生问题一定发生在UserService中的getUserInfoById方法。
进一步,由于用户传入的uid信息没有问题,那么传入getUserInfoById方法也肯定没有问题,所以问题发生地一定在getUserInfoById中获取用户信息的位置。所以不难得出这样的猜测,即问题大概率在 UserInfo info = locals.get()这行代码。
为了加深理解,我们再来回顾一下问题。"即用户A登录,最终却查询到用户B相关的信息"。 其实,这个问题本质其实在于数据不一致。众所周知,造成数据不一致的原因有很多,但归根到底其实无非就是:“存在多线程访问的资源信息,进一步,多线程的存在导致数据状态的改变原因不唯一”。
而Spring中的Bean都是单例的,也就是说Bean中成员信息是共享的。换句话说, 如果Bean中会操纵类的成员变量,那么每次服务请求时,都会对该变量状态进行改变,也就会导致该变量成员那状态不断发生改变。
具体到上述例子,UserService中的被方法操纵的成员是什么?当然是locals这个成员变量啦! 至此,问题其实已经被我们定位到了,导致问题发生的原因在于locals变量。
说到此,你可能你会疑惑ThreadLocal不是可以保证线程安全吗?怎么使用了线程安全的工具包还会导致线程安全问题?
@RestController @RequestMapping("/th") public class UserController { ThreadLocal<Integer> uids = new ThreadLocal<>(); @GetMapping("/u") public CommonResult getUserInfo(Integer uid) { Integer firstId = uids.get(); String firstMsg = Thread.currentThread().getName() + " id is " + firstId; if (firstId == null) { uids.set(uid); } Integer secondId = uids.get(); String secondMsg = Thread.currentThread().getName() + " id is " + secondId; List<String> msgs = Arrays.asList(firstMsg,secondMsg); return CommonResult.success(msgs); } }第一次访问:uid=1
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }可以看到,ThreadLocal的set方法逻辑大致如下:
那有什么解决办法吗?其实很简单,每次使用完记得执行remove方法即可。因为如果不调用remove方法,当面临线程池或其他线程重用机制可能会导致不同任务之间共享ThreadLocal数据,这可能导致意外的数据污染或不一致性。就如我们的例子那样。
至此,我们以一个实际生产中遇到的一个问题为例由浅入深的分析了ThreadLocal使用不规范所带来的线程不安全问题。可以看到排查问题时,我们用到的不仅仅只有ThreadLocal的知识,更有多线程相关的知识。
可能平时我们也会抱怨学了很多线程知识,但工作中却很少使用。因为日常代码中基本写不到多线程相关的功能。但事实却是,很多时候只是我们没有意识到多线程的使用。例如,在Tomcat 这种 Web 服务器下跑的业务代码,本来就运行在一个多线程环境,否则接口也不可能支持这么高的并发,并不能单纯认为没有显式开启多线程就不会有线程安全问题。此外,虽然jdk提供很多线程安全的工具类,但其也有特定的使用规范,如果不遵循规范依旧会导致线程安全问题, 并不是使用了线程安全的工具类就一定不会出问题!