ThreadLocal使用不规范,上线两行泪
点击关注公众号,回复”福利”即可参与文末抽奖ThreadLocal是Java中的一个重要的类,其提供了一种创建线程局部变量机制。从而使得每个线程都有自己独立的副本,互不影响。此外,ThreadLocal也是面试的一个重点,对于此网上已经有很多经典文章来进行分析,但今天我们主要分析笔者在项目中遇到的一个错误使用ThreadLocal的示例,并针对错误原因进行深入剖析,理论结合实践让你更加透彻的理解ThreadLocal的使用。
前言Java中的ThreadLocal是一种用于在多线程环境中存储线程局部变量的机制,它为每个线程都提供了独立的变量副本,从而避免了线程之间的竞争条件。事实上,ThreadLocal的工作原理是在每个线程中创建一个独立的变量副本,并且每个线程只能访问自己的副本。
「进一步,ThreaLocal可以在当前线程中独立的保存信息,这样就方便同一个线程的其他方法获取到该信息。」 因此,ThreaLocal的一个最广泛的使用场景就是将信息保存,从而方便后续方法直接从线程中获取。
使用ThreadLocal出现的问题明白了ThreaLocal的应应用场景后,我们来看一段如下代码:
?控制层
?@RestController
@Slf4j
@RequestMapping("/user")
publicclassUserController{
@Autowire
privateUserServiceuserService;
@GetMapping("get-userdata-byId")
publicCommonResultObjectgetUserData(Integeruid){
returnuserService.getUserInfoById(uid);
}
?服务层
?@Service
publicclassUserService{
ThreadLocalUserInfolocals=newThreadLocal();
publicCommonResultUserInfogetUserInfoById(Stringuid){
UserInfoinfo=locals.get();
if(info==null){
//调用uid查询用户
UserInfouserInfo=UserMapper.queryUserInfoById(uid);
locals.set(userInfo);
}
//....省略后续会利用UserInfo完成某些操作
returnCommonResult.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不是可以保证线程安全吗?怎么使用了线程安全的工具包还会导致线程安全问题?
问题复现况且你说是ThreadLocal出问题那就是ThreadLocal出问题吗?你有证据吗?所以,接下来我们将通过几行简单的代码,复现这个问题。
@RestController
@RequestMapping("/th")
publicclassUserController{
ThreadLocalIntegeruids=newThreadLocal();
@GetMapping("/u")
publicCommonResultgetUserInfo(Integeruid){
IntegerfirstId=uids.get();
StringfirstMsg=Thread.currentThread().getName()+"idis"+firstId;
if(firstId==null){
uids.set(uid);
}
IntegersecondId=uids.get();
StringsecondMsg=Thread.currentThread().getName()+"idis"+secondId;
ListStringmsgs=Arrays.asList(firstMsg,secondMsg);
returnCommonResult.success(msgs);
}
}
第一次访问:uid=1
第二次访问:uid=2可以看到,对于第二次uid=2的访问,这次就出现了 Bug,显然第二次获取到了用户1的信息。其实,从这里就可以看出,我们最开始的猜测没有任何问题。
拆解问题发生原因既然知道了发生问题的原因在于ThreadLocal的使用,那究竟是什么导致了这个问题呢?事实上,我们在使用ThreadLocal时主要就是使用了其的get/set方法,这就是我们分析的切入口。先来看下ThreadLocal的set方法。
publicvoidset(Tvalue){
Threadt=Thread.currentThread();
ThreadLocalMapmap=getMap(t);
if(map!=null)
map.set(this,value);
else
createMap(t,value);
}
可以看到,ThreadLocal的set方法逻辑大致如下:
首先,通过Thread.currentThread获取到当前的线程然后,获取到线程当中的属性ThreadLocalMap。接着,对ThreadLocalMap进行判断,如果不为空,就直接更新要保存的变量值;否则,创建一个threadLocalMap,并且完成赋值。进一步,下图展示了Thrad,ThreadLocal,ThredLocalMap三者间的关系。
回到我们例子,那导致出现访问错乱的原因是什么呢?其实很简单,原因就是 Tomcat 内部会维护一个线程池,从而使得线程被重用。从图中可以看到两次请求的线程都是同一个线程: http-nio-8080-exec-1,所以导致数据访问出现错乱。
那有什么解决办法吗?「其实很简单,每次使用完记得执行remove方法即可」。因为如果不调用remove方法,当面临线程池或其他线程重用机制可能会导致不同任务之间共享ThreadLocal数据,这可能导致意外的数据污染或不一致性。就如我们的例子那样。
总结至此,我们以一个实际生产中遇到的一个问题为例由浅入深的分析了ThreadLocal使用不规范所带来的线程不安全问题。可以看到排查问题时,我们用到的不仅仅只有ThreadLocal的知识,更有多线程相关的知识。
可能平时我们也会抱怨学了很多线程知识,但工作中却很少使用。因为日常代码中基本写不到多线程相关的功能。但事实却是,很多时候只是我们没有意识到多线程的使用。例如,在Tomcat 这种 Web 服务器下跑的业务代码,本来就运行在一个多线程环境,否则接口也不可能支持这么高的并发,并不能单纯认为没有显式开启多线程就不会有线程安全问题。此外,虽然jdk提供很多线程安全的工具类,但其也有特定的使用规范,如果不遵循规范依旧会导致线程安全问题, 「并不是使用了线程安全的工具类就一定不会出问题!」
最后,再多提一嘴,学了的知识一定要用起来,可能你为了应付面试也曾看过ThreadLocal相关的面经,也知道使用ThreadLocal要执行remove,否则可能会导致内存泄露,「但编程的很多东西,确实需要自己实际操作,否则知识并不会凭空进入你的脑海。」
选择了程序员这条路,注定只能不断的学习,大家一起共勉啦!另外,祝大家双节快乐!
点击小卡片,参与粉丝专属福利!!
如果文章对你有帮助的话欢迎
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线