什么?!90% 的 ThreadLocal 都在滥用或错用!
最近在看一个系统代码时,发现系统里面在使用到了 ThreadLocal,乍一看,好像很高级的样子。我再仔细一看,这个场景并不会存在线程安全问题,完全只是在一个方法中传参使用的啊!(震惊)
难道是我水平太低,看不懂这个高级用法?经过和架构师请教和确认,这完全就是一个 ThreadLocal 滥用的典型案例啊!甚至,日常的业务系统中,90%以上的 ThreadLocal 都在滥用或错用!快来看看说的是不是你~
ThreadLocal 简介
ThreadLocal 也叫线程局部变量,是 Java 提供的一个工具类,它为每个线程提供一个独立的变量副本,从而实现线程间的数据隔离。
ThreadLocal 中的关键方法如下:
public T get()
:返回当前线程所对应线程局部变量|public void set(T value)
:设置当前线程的线程局部变量的值|public void remove()
:删除当前线程局部变量的值|
滥用:无伤大雅
在一些没有必要进行线程隔离的场景中使用“好像高级”的 ThreadLocal,看起来是挺唬人的,但这其实就是“纸老虎”。
滥用的典型案例是:在一个方法的内部,将入参信息写入 ThreadLocal 进行保存,在后续需要时从 ThreadLocal 中取出使用。一段简单的示例代码,可以参考:
为什么说无伤大雅呢?因为这段代码的写入 ThreadLocal 和读取 ThreadLocal 都是在同一个线程中进行的,代码可以正常运行,并且运行结果正确。
但是,还是这段代码,也埋了一个“坑”,稍有不慎,将可能导致错误的结果。如果在处理业务逻辑中(①或者②处)使用了多线程技术,创建了其他线程,在其他线程中去获取 ThreadLocal 中写入的值,根据获取到的值进行相关业务逻辑处理,很可能得到预期之外的结果,从而演化为一个错误案例。
错用:血泪教训
错误案例
以一个常见的 Web 应用为例,方便起见,我在本机 Idea 使用 Spring Boot 创建一个工程,在 Controller 中使用 ThreadLocal 来保存线程中的用户信息,初识为 null。业务逻辑很简单,先从 ThreadLocal 获取一次值,然后把入参中的 uid 设置到 ThreadLocal 中,随后再获取一次值,最后返回两次获得的 uid。代码如下:
启动工程,使用 uid=1,uid=2 ……作为入参进行测试,结果如下:
http://localhost:8080/user?uid=1 :没有问题!
http://localhost:8080/user?uid=2 :很稳!
多来几次,结果还是很稳的。
结果符合预期,这真的没有问题吗?
问到这里,你是不是也有点怀疑了?是不是我要翻车了?写到这里就被迫结束了。NO!NO!NO!继续看!
我调整 application.properties 参数,方便复现问题:
继续执行上面的测试:
http://localhost:8080/user?uid=1 :没有问题!
http://localhost:8080/user?uid=2 :什么?uid2 读取到了 uid1 的信息!!!
http://localhost:8080/user?uid=1 :什么?uid1 也读取到了 uid2 的信息!!!
这岂不是乱套了,全乱了,整个晋西北都乱成了一锅粥!
问题原因
为什么数据会错乱呢?
数据错乱,究竟是怎么回事呢?按理说,在设置用户信息之前第一次获取的值始终应该是 null,然后设置之后再去读取,读到的应该是设置之后的值才对啊。
真相是这样的,程序运行在 Tomcat 中,Tomcat 的工作线程是基于线程池的,线程池其实是复用了一些固定的线程的。
如果线程被复用,那么很可能从 ThreadLocal 获取的值是之前其他用户的遗留下的值。
为什么调整线程池参数,就测试出问题了呢?
Spring Boot 内嵌的 Tomcat 服务器的默认线程池最大线程数是 200,但通过修改 application.properties
或 application.yml
文件来调整。关键参数如下:
最大工作线程数 (server.tomcat.max-threads):默认值为 200,Tomcat 可以同时处理的最大线程数。
最小工作线程数 (server.tomcat.min-spare-threads):默认值为 10,Tomcat 在启动时初始化的线程数。
最大连接数 (server.tomcat.max-connections):默认值为 10000,Tomcat 在任何时候可以接受的最大连接数。
等待队列长度 (server.tomcat.accept-count):默认值为 100,当所有线程都在使用时,等待队列的最大长度。
我调整参数(server.tomcat.max-threads=1)之后,很容易复用到之前的线程,复用线程情况下,触发了代码中隐藏的 Bug。
如果不调整的话,在较大流量的场景下也会触发这个 Bug。
解决办法
那应该如何修改呢?其实方案很简单,在 finally 代码块中显式清除 ThreadLocal 中的数据。这样,即使复用了之前的线程,也不会获取到错误的用户信息。修正后的代码如下:
正确使用
前面是滥用和错用的例子,那应该如何正确使用 ThreadLocal 呢? 正确的使用场景包括:
在网关场景下,使用 ThreadLocal 来存储追踪请求的 ID、请求来源等信息;
RPC 等框架中使用 ThreadLocal 保存请求上下文信息,例如压测标识等;
……
最常见的案例是用户登录拦截,从 HttpServletRequest 获取到用户信息,并保存到 ThreadLocal 中,方便后续随时取用,代码如下:
总结
本文给大家介绍了 ThreadLocal 的无伤大雅的滥用案例、血泪教训的错误案例,分析问题原因和解决方法,也给出了正确的案例,希望对大家理解和使用 ThreadLocal 有帮助。
真正的高手往往使用最朴实无华的招数,写出无可挑剔的代码;有时候炫技式的代码可能会出错。
大师级程序员把系统当作故事来讲,而不是当作程序来写。把故事讲好,即方便自己阅读,也方便别人阅读,共勉。
一起学习
欢迎各位在评论区或者私信我一起交流讨论,或者加我主页 weixin,备注技术渠道(如 InfoQ),进入技术交流群,我们一起讨论和交流,共同进步!
也欢迎大家关注我的 InfoQ、公众号(码上暴富),点赞、留言、转发。你的支持,是我更文的最大动力!
版权声明: 本文为 InfoQ 作者【James_Shangguan】的原创文章。
原文链接:【http://xie.infoq.cn/article/05000bd9b78a53253fa037255】。文章转载请联系作者。
评论