最近在看一个系统代码时,发现系统里面在使用到了 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>();

    @Override
public 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;
} @Override
public void postHandle(HttpServletRequest request, HttpServletResponse resposne, Object o,
ModelAndView modelAndView) throws Exception {
} @Override
public 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,备注技术渠道(如博客园),进入技术交流群,我们一起讨论和交流,共同进步!

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

什么?!90%的ThreadLocal都在滥用或错用!的更多相关文章

  1. 《.NET 5.0 背锅案》第5集-案情大转弯:都是我们的错,让 .NET 5.0 背锅

    第1集:验证 .NET 5.0 正式版 docker 镜像问题 第2集:码中的小窟窿,背后的大坑,发现重要嫌犯 EnyimMemcachedCore 第3集-剧情反转:EnyimMemcachedCo ...

  2. 90%的开发者都不知道的UI本质原理和优化方式

    前言 很多开发者在工作中一直和UI打交道,所以认为UI非常的简单! 事实上对于90%的开发者来说,不知道UI的本质原理. 虽然在开发中,我们在接到产品的UI需求之后,可以走捷径照抄大型APP代码,但是 ...

  3. 为什么90%的CTO 都做不好绩效管理

    ​ 十多年从业经历,从 2001 年开始带团队到现在,我几乎经历过所有的 IT 角色.2010 年,我随创始团队筹建国美在线至今,经历了从几百单到现在日均百万订单,从只有家电品类到现在全品类.金融.大 ...

  4. Java引用类型原理深度剖析,看完文章,90%的人都收藏了

    本文为synchronized系列第二篇.主要内容为分析偏向锁的实现. 偏向锁的诞生背景和基本原理在上文中已经讲过了. 本文将分为几块内容: 1.偏向锁的入口 2.偏向锁的获取流程 3.偏向锁的撤销流 ...

  5. 90% 前端开发者都不知道的 JavaScript 实用小技巧

    面试神器之数组去重 const a = [...new Set([1, 2, 3, 3])] >> [1, 2, 3] 操作数组担心 falsy 值? const res = myArra ...

  6. 90%程序员都没有完全答对Cookie和Session的区别

    我在做面试官的时候,曾经问过很多朋友这个问题: Cookie 和 Session 有什么区别呢?大部分的面试者应该都可以说上一两句,比如:什么是 Cookie?什么是 Session?两者的区别等. ...

  7. 90%的开发都没搞懂的CI和CD!

    据IDC统计,2017年,DevOps的全球软件市场已达到约39亿美元的水平,预计到2022年市场将达到80亿美元左右! 在敏捷软件开发环境中,工作模型和操作需要对公司不断变化的需求具有超级灵活的能力 ...

  8. 为什么90%的大学都要求计算机专业学习C语言?

    编程语言是编程的工具,计算机相关专业的学生必须具备足够的编程能力.当然,关于"最好语言"的争论从来没有休止过,这里要强调一下:语言的选择真的没那么重要,学习语言的过程最重要是语言的 ...

  9. PriorityQueue 是线性结构吗?90%的人都搞错了!

    文章首发于「陈树义」公众号及个人博客 shuyi.tech 其实这个问题的完整描述是:Java 中的 PriorityQueue 实现,其数据的逻辑结构是线性结构吗?其数据的物理结构又是什么? 估计很 ...

  10. 90%的人都不知道的Node.js 依赖关系管理(上)

    转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 原文参考:https://dzone.com/articles/nodejs-dependency-mana ...

随机推荐

  1. JavaSE 常见时间日期

    java.util包提供了Date类来封装当前的⽇期和时间 构造函数 //当前时间 Date() //从1970年1⽉1⽇起的毫秒数作为参数 Date(long millisec) 常见方法 //返回 ...

  2. redis-sort排序

    sort命令可以对列表(lpush rpush).集合(sadd srem).有序集合(zadd)进行排序 1.复习一下三种数据结构(1)列表 lpush  key val1 val2 val3 .. ...

  3. Midnight Commander (MC)

    Midnight Commander GNU Midnight Commander 是一个可视化文件管理器,根据 GNU 通用公共许可证获得许可,因此有资格成为自由软件.它是一个功能丰富的全屏文本模式 ...

  4. Vue 新增不参与打包的接口地址配置文件

    Vue 新增不参与打包的接口地址配置文件   by:授客 QQ:1033553122   开发环境   Win 10   Vue 2.5.2 问题描述 vue工程项目,npm run build we ...

  5. Nuxt.js必读:轻松掌握运行时配置与 useRuntimeConfig

    title: Nuxt.js必读:轻松掌握运行时配置与 useRuntimeConfig date: 2024/7/29 updated: 2024/7/29 author: cmdragon exc ...

  6. oracle数据库状态查询

    oracle数据库状态查询(表空间,表空间利用率,是否自动扩展,IOPS,MBPS,数据库总大小) 排除了('SYSAUX', 'USERS', 'SYSTEM','UNDOTBS1') 结果示例 - ...

  7. 趣谈n++与++n的differences

    前言:今天小白在学习时无意发现一组有趣的孪生兄弟** n++ 与 ++n** 探索:二者不同之处 工具:VS2022 过程: 前者是++n,输出11,12,13,14 后者是n++,输出10,11,1 ...

  8. 对国内的人行机器人(humanoid)的一些技术类的提问?

    贵公司产品在机器人仿真时具体采用的仿真软件(NVIDIA家的.开源的webot,等等)是哪款,如果没有使用NVIDIA全家桶那么其原因是什么(如:技术依赖没有独立技术.技术栈太过于复杂暂时没有精力和能 ...

  9. 强化学习算法真的适合于你的应用吗 —— 强化学习研究方向(研究领域)现有的不足(短板、无法落地性) —— Why You (Probably) Shouldn’t Use Reinforcement Learning

    外文原文: Why You (Probably) Shouldn't Use Reinforcement Learning 地址: https://towardsdatascience.com/why ...

  10. Ubuntu18.04 server 双网卡,一个设置为静态IP, 一个设置为动态IP

    操作目的如题: 修改网络配置文件: sudo vim /etc/netplan/50-cloud-init.yaml 修改内容如下: # This file is generated from inf ...