最近在看一个系统代码时,发现系统里面在使用到了 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. WPS中导入endnote插件

    WPS中导入endnote插件 1. 找到 Endnote 插件的目录: D:\Program Files (x86)\EndNote 20\Product-Support\CWYW   2. 把Cw ...

  2. 使用bootchart 对 高通Android 进行性能分析

    使用bootchart 对 高通Android 进行性能分析 Android版本:7.0 适用平台:高通和MTK 参考: https://blog.csdn.net/qq_19923217/artic ...

  3. 劫持TLS绕过canary && 堆和栈的灵活转换

    引入:什么是TLScanary? TLScanary 是一种在 Pwn(主要是二进制漏洞利用)中常见的技术,专门用于处理 TLS 保护的二进制文件.在安全竞赛(例如 CTF)和漏洞利用场景中,攻击者需 ...

  4. 全网最适合入门的面向对象编程教程:11 类和对象的Python实现-子类调用父类方法-模拟串口传感器和主机

    全网最适合入门的面向对象编程教程:11 类和对象的 Python 实现-子类调用父类方法-模拟串口传感器和主机 摘要: 本节课,我们主要讲解了在 Python 类的继承中子类如何进行初始化.调用父类的 ...

  5. 深入解读RabbitMQ工作原理

    RabbitMQ简介 在介绍RabbitMQ之前首先要介绍一下MQ,MQ是什么?MQ全称是Message Queue,可以理解为消息队列的意思. RabbitMQ是一个实现了AMQP(Advanced ...

  6. Profibus_DP转ModbusTCP网关模块接马保通讯案例

    某工业企业为了提高生产效率和管理水平,决定对其生产线进行智能化改造.在该项目中,利用巴图自动化Profibus_DP转ModbusTCP网关模块(BT-ETHPB20)连接了不同生产设备,实现了设备之 ...

  7. [oeasy]python0079_控制序列_光标位置设置_ESC_逃逸字符_CSI

    光标位置 回忆上次内容 上次我们研究的比较杂 类型转化 进制转化 捕获异常 版本控制 生成帮助文档 变量的常用类型 变量的生命周期控制   数据类型主要研究了两个 字符串 str   整型数字 int ...

  8. [oeasy]python0043_八进制_oct_octal_october_octave

    八进制(oct) 回忆上次内容 什么是 转义? 转义转义 转化含义 \ 是 转义字符 \n.\r是 转义序列 还有什么 转义序列 吗? \a是 响铃 \b 退格键 \t 水平制表符 tab键 \v.\ ...

  9. oeasy教您玩转vim - 72 - # 缩写abbreviation

    ​ 缩写abbreviation 回忆上次折叠的细节 这次了解到了:mkview.:loadview 保存和加载视图 可以把当前的状态保存下来 可以在 viewoption 中配置保存选项,设置哪些需 ...

  10. 只会建数据库怎么写API?database2api 能帮到你!

    database2api 意为 DataBase to API,即只要有数据库,就可以生成开放 API. database2api 是一款强大而便捷的工具,主要功能是依据现有的数据库自动生成开放的 A ...