什么?!90%的ThreadLocal都在滥用或错用!

news/2024/9/18 8:56:55/文章来源:https://www.cnblogs.com/sgh1023/p/18375055

最近在看一个系统代码时,发现系统里面在使用到了 ThreadLocal,乍一看,好像很高级的样子。我再仔细一看,这个场景并不会存在线程安全问题,完全只是在一个方法中传参使用的啊!(震惊)

难道是我水平太低,看不懂这个高级用法?经过和架构师请教和确认,这完全就是一个 ThreadLocal 滥用的典型案例啊!甚至,日常的业务系统中,90%以上的 ThreadLocal 都在滥用或错用!快来看看说的是不是你~

ThreadLocal 简介

ThreadLocal 也叫线程局部变量,是 Java 提供的一个工具类,它为每个线程提供一个独立的变量副本,从而实现线程间的数据隔离

ThreadLocal 中的关键方法如下:

方法定义 方法用途
public T get() 返回当前线程所对应线程局部变量
public void set(T value) 设置当前线程的线程局部变量的值
public void remove() 删除当前线程局部变量的值

滥用:无伤大雅

在一些没有必要进行线程隔离的场景中使用“好像高级”的 ThreadLocal,看起来是挺唬人的,但这其实就是“纸老虎”。

滥用的典型案例是:在一个方法的内部,将入参信息写入 ThreadLocal 进行保存,在后续需要时从 ThreadLocal 中取出使用。一段简单的示例代码,可以参考:

public class TestService {private static final String COMMON = "1";private ThreadLocal<Map<String, Object>> commonThreadLocal = new ThreadLocal<>();public void testThreadLocal(String commonId, String activityId) {setCommonThreadLocal(commonId, activityId);// 省略业务代码①doSomething();// 省略业务代码②}/*** 将入参写入 ThreadLocal** @param commonId* @param activityId*/private void setCommonThreadLocal(String commonId, String activityId) {Map<String, Object> params = new HashMap<>();params.put("commonId", commonId);params.put("activityId", activityId);this.commonThreadLocal.set(params);}/*** 从 ThreadLocal 取出参数,进行业务处理*/private void doSomething() {Map<String, Object> params = this.commonThreadLocal.get();String commonId = (String) params.get("commonId");if (StringUtils.equals(commonId, COMMON)) {// 省略业务代码}}
}

为什么说无伤大雅呢?因为这段代码的写入 ThreadLocal 和读取 ThreadLocal 都是在同一个线程中进行的,代码可以正常运行,并且运行结果正确。

但是,还是这段代码,也埋了一个“坑”,稍有不慎,将可能导致错误的结果。如果在处理业务逻辑中(①或者②处)使用了多线程技术,创建了其他线程,在其他线程中去获取ThreadLocal中写入的值,根据获取到的值进行相关业务逻辑处理,很可能得到预期之外的结果,从而演化为一个错误案例

错用:血泪教训

错误案例

以一个常见的 Web 应用为例,方便起见,我在本机 Idea 使用 Spring Boot 创建一个工程,在 Controller 中使用 ThreadLocal 来保存线程中的用户信息,初识为 null。业务逻辑很简单,先从 ThreadLocal 获取一次值,然后把入参中的 uid 设置到 ThreadLocal 中,随后再获取一次值,最后返回两次获得的 uid。代码如下:

private static final ThreadLocal<String> USER_INFO_THREAD_LOCAL = ThreadLocal.withInitial(() -> null);@RequestMapping("user")
public String user(@RequestParam("uid") String uid) {//查询 ThreadLocal 中的用户信息String before = USER_INFO_THREAD_LOCAL.get();//设置用户信息USER_INFO_THREAD_LOCAL.set(uid);//再查询一次 ThreadLocal 中的用户信息String after = USER_INFO_THREAD_LOCAL.get();return before + ";" + after;
}

启动工程,使用 uid=1,uid=2 ……作为入参进行测试,结果如下:

http://localhost:8080/user?uid=1 :没有问题!

http://localhost:8080/user?uid=2 :很稳!

多来几次,结果还是很稳的。

结果符合预期,这真的没有问题吗?

问到这里,你是不是也有点怀疑了?是不是我要翻车了?写到这里就被迫结束了。NO!NO!NO!继续看!

我调整 application.properties 参数,方便复现问题:

server.tomcat.max-threads=1

继续执行上面的测试:

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.propertiesapplication.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 中的数据。这样,即使复用了之前的线程,也不会获取到错误的用户信息。修正后的代码如下:

private static final ThreadLocal<String> USER_INFO_THREAD_LOCAL = ThreadLocal.withInitial(() -> null);@RequestMapping("right")
public String right(@RequestParam("uid") String uid) {String before = USER_INFO_THREAD_LOCAL.get();USER_INFO_THREAD_LOCAL.set(uid);try {String after = USER_INFO_THREAD_LOCAL.get();return before + ";" + after;} finally {USER_INFO_THREAD_LOCAL.remove();}
}

正确使用

前面是滥用和错用的例子,那应该如何正确使用 ThreadLocal 呢? 正确的使用场景包括:

  1. 在网关场景下,使用 ThreadLocal 来存储追踪请求的 ID、请求来源等信息;
  2. RPC 等框架中使用 ThreadLocal 保存请求上下文信息;
  3. ……

最常见的案例是用户登录拦截,从 HttpServletRequest 获取到用户信息,并保存到 ThreadLocal 中,方便后续随时取用,代码如下:

public class ContextHttpInterceptor implements HandlerInterceptor {private static final ThreadLocal<Context> contextThreadLocal = new ThreadLocal<Context>();@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {try {Context context = new Context();String pin = request.getParameter("pin");if (StringUtils.isNotBlank(pin)) {context.setPin(pin);}contextThreadLocal.set(context);} catch (Exception e) {}return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse resposne, Object o,ModelAndView modelAndView) throws Exception {}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse resposne,Object o, Exception e) throws Exception {contextThreadLocal.remove();}
}public class Context {private String pin;public String getPin() {return pin;}public void setPin(String pin) {this.pin = pin;}
}

总结

本文给大家介绍了 ThreadLocal 的无伤大雅的滥用案例、血泪教训的错误案例,分析问题原因和解决方法,也给出了正确的案例,希望对大家理解和使用 ThreadLocal 有帮助。

真正的高手往往使用最朴实无华的招数,写出无可挑剔的代码;有时候炫技式的代码可能会出错。

大师级程序员把系统当作故事来讲,而不是当作程序来写。把故事讲好,即方便自己阅读,也方便别人阅读,共勉。

一起学习

欢迎各位在评论区或者私信我一起交流讨论,或者加我主页 weixin,备注技术渠道(如博客园),进入技术交流群,我们一起讨论和交流,共同进步!

也欢迎大家关注我的博客园、公众号(码上暴富),点赞、留言、转发。你的支持,是我更文的最大动力!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/785920.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

半导体行业的数据革命:高性能数据采集系统如何改变游戏规则!

随着国产替代和半导体工厂扩产潮,半导体工厂有着大量建厂和设备管理需求,设备数据采集作为现代工业生产与管理的关键环节,其重要性不言而喻,数字化转型正深刻推动半导体制造企业实现远程监控、提高生产效率、降低生产成本、优化产品质量及明晰精细化方向。 半导体机台设备…

除了按值和引用,方法参数的第三种传递方式

参数在方法种具有按“值(by value)”和“引用(by ref)”两种传递方式,这是每个.NET程序员深入骨髓得基本概念。但是我若告诉你,.NET规定的参数传递形式其实是三种,会不会颠覆你的认知。参数在方法种具有按“值(by value)”和“引用(by ref)”两种传递方式,这是每个…

强!34.1K star! 再见Postman,新一代API测试利器,功能强大、颜值爆表!

1、引言 在当今的互联网时代,API(应用程序编程接口)已经成为连接不同软件系统的桥梁。作为一名开发者,掌握API测试技能至关重要。市面上的API测试工具琳琅满目,今天我们要介绍的是一款开源、跨平台的API测试工具——Insomnia。 2、Insomnia介绍 Insomnia是一款功能丰富、易…

Modbus初学者教程,第五章:Modbus 中的功能码和异常码

第五章:Modbus 中的功能码和异常码平时调试Modbus设备,或者学习Modbus协议,推荐一款Modbus主从站模拟器: 主站下载地址:Modbus从站模拟器 从站下载地址:Modbus主站模拟器可视化 Modbus 设备中的数据 Modbus 从设备可以想象为具有一个内部电子表格,其中填满了数字。Modbu…

袋鼠

先转化一下题意:求有多少个1~n的排列p能够满足 \(\forall i \in (1,n)\) ,使 $ p_{i} $ 左右两边的数同时小于或者大于 \(p_{i}\) ,并且\(p_{1}=s,p_{n}=t\) 。 比较明显的预设型DP(连转化题意我都做不到,悲),先正常来分析一下,我们填数从小往大枚举,如果我们填入一个数,…

JetBrains Writerside 2024.2 (macOS, Linux, Windows) - 编写、测试、构建和发布最佳教程

JetBrains Writerside 2024.2 (macOS, Linux, Windows) - 编写、测试、构建和发布最佳教程JetBrains Writerside 2024.2 (macOS, Linux, Windows) - 编写、测试、构建和发布最佳教程 JetBrains 跨平台开发者工具 请访问原文链接:https://sysin.org/blog/jetbrains-writerside/…

Citrix ADC Release 13.1 Build 54.29 (nCore, VPX, SDX, CPX, BLX) - 混合多云应用交付控制器

Citrix ADC Release 13.1 Build 54.29 (nCore, VPX, SDX, CPX, BLX) - 混合多云应用交付控制器Citrix ADC Release 13.1 Build 54.29 (nCore, VPX, SDX, CPX, BLX) - 混合多云应用交付控制器 Citrix ADC - 混合多云应用交付控制器 请访问原文链接:https://sysin.org/blog/citr…

Metasploit Pro 4.22.3-2024081901 (Linux, Windows) - 专业渗透测试框架

Metasploit Pro 4.22.3-2024081901 (Linux, Windows) - 专业渗透测试框架Metasploit Pro 4.22.3-2024081901 (Linux, Windows) - 专业渗透测试框架 Rapid7 Penetration testing, release Aug 19, 2024 请访问原文链接:https://sysin.org/blog/metasploit-pro-4/,查看最新版。…

dotnet X11 栈空间被回收导致调用 XPutShmImage 闪退

本文记录在使用 X11 过程中的问题,由于不正确使用导致栈空间被回收,从而在调用 XPutShmImage 时让应用闪退,此问题本质上讲只和 X11 的设计有一分钱关系,更多的问题在于我的写法上在 上一篇博客 里,介绍了使用 MIT-SHM 共享内存推送图片,详细请看:dotnet X11 简单使用 M…

Avalonia 11.1 已知问题 IterationCount 为 Infinite 的动画播放出现异常

如果在 Avalonia 后台代码播放一个动画,这个动画的 Animation 的 IterationCount 被设置为 Infinite 那么将在播放的时候抛出 InvalidOperationException 异常本文所使用的 Avalonia 为 11.1.0 版本,由于 Avalonia 行为和 API 变动较多,如大家使用其他版本还请重新测试 如以…

11 Large Companies That Use Java

https://terenbro.com/blog/11-large-companies-that-use-javaWhat is Java? Why do Companies Use Java? 11 Companies That Use Java Overview List of Companies That Use Java Consider Terenbro as Your Trusted Partner Final ThoughtsTOP-5 PostsPopular Java Use Cas…