Best practice-ThreadLocal高并发场景的最佳实践

news2025/3/21 5:17:36

关于ThreadLocal基础信息

引用一段来自ThreadLocal源码中的doc注释来说明其特性:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e. g., a user ID or Transaction ID).

个人理解:该类提供了一个线程级别的变量,不同线程间通过get/set方法可以访问到自己不同的数据副本,ThreadLocal实例通常是类中的私有的静态数据如userId或事务id。

ThreadLocalJava中是一个线程级别的变量,每一个线程都有自己独立的变量,ThreadLocal类型所创建,key是线程信息,value值是数据的副本,其副本值在各个线程间不可见,在并发模式下各个线程会访问到自己对应副本的数据信息。

通过以上特性信息,可以将ThreadLoacl应用在获取信息较为昂贵且上下文中多次使用到的业务场景,用户信息的存储和缓存场景就很适合使用该类。

线程池中的线程重用

前置知识

Spring Boot虽然没有明确配置Tomcat的地方,实则不同Spring Boot版本内置了Tomcat,不同Spring Boot版本与Tomcat版本呈现绑定关系。以下用例为方便复现Spring Boot项目中的线程复用问题,需要手动将Spring Boot的线程池大小设置为1,以保证每次请求都是同一个线程处理请求。Spring Boot 2.1.x版本之后内置Tomcat 9.x,Spring Boot 3.x版本后内置Tomcat 10.x,修properties配置文件为例,修改线程池大小的代码为:

server.tomcat.threads.max=1

yaml配置文件写法:

server:
  tomcat:
    threads:
      max: 1

Spring Boot版本嵌入式Tomcat版本对应关系:

Spring Boot 版本默认 Tomcat 版本
1.5.x8.5.x
2.0.x8.5.x
2.1.x9.0.x
2.2.x9.0.x
2.3.x9.0.x
2.4.x9.0.x
2.5.x9.0.x
2.6.x9.0.x
2.7.x9.0.x
3.0.x10.1.x
3.1.x10.1.x
3.2.x10.1.x

Spring Boot 2.0.x版本配置:

server.tomcat.max-threads=1

错误实践业务场景

ThreadLocal 在多线程环境下可能导致的线程复用问题,即ThreadLocal没有被清理的情况下,上一个请求的 ThreadLocal 值可能会影响下一个请求

以下代码示例使用ThreadLocal存储用户信息,使用ThreadLocal.withInitial()进行初始化,before表示请求前的值,设置用户信息之前先查询一次ThreadLocal中的用户信息,after表示赋值到ThreadLocal,使用ThreadLocal保存的副本变量。

// String类型表示用户信息
private static final ThreadLocal<String> requestIdThreadLocal = ThreadLocal.withInitial(() -> null);

@GetMapping("/threadlocal")
public Map<String, String> test(@RequestParam("requestId") String requestId) {
    // 查询ThreadLocal中是否已经有值
    String before = Thread.currentThread().getName() + " : " + requestIdThreadLocal.get();

    // 设置请求 ID
    requestIdThreadLocal.set(requestId);

    // 再次查询
    String after = Thread.currentThread().getName() + " : " + requestIdThreadLocal.get();

    // 模拟业务逻辑处理
    processRequest();

    // 这里如果不清理,可能会影响后续请求
    // requestIdThreadLocal.remove();  // ✅ 推荐清理

    // 返回数据
    Map<String, String> result = new HashMap<>();
    result.put("before", before);
    result.put("after", after);
    return result;
}

private void processRequest() {
    // 模拟一些业务处理
    try {
        TimeUnit.MILLISECONDS.sleep(100);
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
    }
}

假设id为2的用户登录系统开始第一次请求接口,ThreadLocal中没有用户2的信息,所以before是null,模拟业务处理从数据库表中加载出用户信息,再次从ThreadLocal中获取用户信息,会正常获取到用户信息:

在这里插入图片描述

此时再来用户1请求该接口:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

用户1请求到的初始值是2,在从数据库中获取到的用户信息是1,这个在业务上是错误的❎,即一开始的初始值获取到了其他线程的用户信息,造成该问题的原因是:Spring Boot的线程池中的线程是可以复用的,假设线程id为1的线程在代码执行完毕后,对应的ThreadLocal副本变量值没有被清空,当线程池中的线程被复用时,该线程就会获取到历史请求获取到的副本变量。

解决方案就是在每次使用完成后,将副本变量进行清空。

requestIdThreadLocal.remove();

requestIdThreadLocal.remove();的注释放开即可,重启服务,再次请求:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

ThreadLocal最佳实践

针对Threadlocal的避坑使用方式,以下是在存储用户信息业务场景中的最佳实践,代码可参考:

// 1. 定义静态的 ThreadLocal 变量(推荐使用 static final)且 重写 initialValue 提供默认值(可选)
private static final ThreadLocal<Integer> currentUser = ThreadLocal.withInitial(() -> 1);

@GetMapping("/best/way")
    public void bestWay() {
        // 提交多个任务
        for (int i = 0; i < 5; i++) {
            final int index = i;
            executor.execute(() -> {
                // 每个任务使用不同的用户上下文
                String before = Thread.currentThread().getName() + " : " + currentUser.get();

                // 设置请求 ID
                currentUser.set(index);

                // 再次查询
                String after = Thread.currentThread().getName() + " : " + currentUser.get();

                // 模拟业务逻辑处理
                processRequest();

                // ✅ 推荐清理,这里如果不清理,可能会影响后续请求
                currentUser.remove();

                log.info("before : " + before + " after : " + after);
            });
        }
        executor.shutdown();
    }

DEMO中使用两个线程处理5个请求,模拟并发场景,覆盖上文中ThreadLocal在线程复用场景下获取到其他用户信息的问题,以下是执行结果:

before值为1,是因为在初始化ThreadLocal时将数据定义为了1。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

如不使用remove()方法清理ThreadLocal中的副本值,则会出现上文中获取到其他用户信息问题:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

最佳实践说明

  1. 静态化ThreadLocal变量

    • 使用 static final 定义 ThreadLocal,避免多次创建实例,减少内存占用。
  2. 避免线程池污染

    • 在线程池场景中,线程会被重用,必须显式清理 ThreadLocal,防止旧数据残留,并且在 try-finally 块中调用 remove(),确保即使发生异常也能清理资源。
  3. 初始化默认值

    • 通过重写 initialValue() 提供默认值,避免 get() 时返回 null

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【鸿蒙HarmonyOS Next实战开发】多媒体视频播放-GSYVideoPlayer

简介 GSYVideoPlayer是一个视频播放器库&#xff0c;支持切换内核播放器&#xff08;IJKPlayer、avplayer&#xff09;&#xff0c;并且支持视频截图能力、 视频生成gif能力、边播边缓存能力、视频全屏能力等多种能力。 效果展示&#xff1a; 下载安装 ohpm install ohos/gs…

IDEA中常见问题汇总

&#x1f353; 简介&#xff1a;java系列技术分享(&#x1f449;持续更新中…&#x1f525;) &#x1f353; 初衷:一起学习、一起进步、坚持不懈 &#x1f353; 如果文章内容有误与您的想法不一致,欢迎大家在评论区指正&#x1f64f; &#x1f353; 希望这篇文章对你有所帮助,欢…

基于蜘蛛蜂优化算法的无人机集群三维路径规划Matlab实现

代码下载&#xff1a;私信博主回复基于蜘蛛蜂优化算法的无人机集群三维路径规划Matlab实现 《基于蜘蛛蜂优化算法的无人机集群三维路径规划》 摘要 本研究针对无人机集群三维路径规划问题&#xff0c;提出了一种基于蜘蛛蜂优化算法的解决方案。以5个无人机构成的集群为研究对…

Deepseek系列从v3到R易背面经版

deepseek v3 base要点 MTP : Multi-Token Prediction 训练时&#xff1a; 1. 把前一个block中input tokens经过embedding layer和transformer block的输出&#xff0c;进入output head之前的内容记为h&#xff0c;与下一个block的input tokens经过embedding layer输出的内容都…

Maven入门核心知识点总结

Maven 1. POM&#xff08;Project Object Model&#xff09;2. 坐标&#xff08;Coordinates&#xff09;3. 依赖管理&#xff08;Dependency Management&#xff09;4. 常用五个生命周期&#xff08;Life Circle&#xff09;5. Maven 仓库&#xff08;Maven Repository&#x…

Blocked aria-hidden on an element because its descendant retained focus.

在使用el-popover和el-radio-group实现弹窗选择数据后调用el-popover的doClose()方法时一直报错&#xff01; 经过分析发现el-popover及el-radio__original有aria-hidden属性&#xff0c;具体aria-hidden属性应用自行搜索了解。既然是这个玩意引起的&#xff0c;则在显示时将a…

python爬虫--简单登录

1&#xff0c;使用flask框架搭建一个简易网站 后端代码app.py from flask import Flask, render_template, request, redirect, url_for, sessionapp Flask(__name__) app.secret_key 123456789 # 用于加密会话数据# 模拟用户数据库 users {user1: {password: password1}…

三次握手,四次挥手,服务器模型(多进程并发,线程),基于套接字的UDP通信

三次握手&#xff1a; 第一次握手&#xff1a;客户端向服务器发送SYN待确认数据x, 客户端进入SYN_SEND状态​ 第二次握手&#xff1a;服务器向客户端回传一条ACK应答数据x1, 同时发送一条SYN待确认数据y&#xff0c;服务器进入SYN_RECV状态​ 第三次握手&#xff1a;客户端向服…

Linux TCP 编程详解与实例

一、引言 在网络编程的领域中&#xff0c;TCP&#xff08;Transmission Control Protocol&#xff09;协议因其可靠的数据传输特性而被广泛应用。在 Linux 环境下&#xff0c;使用 C 或 C 进行 TCP 编程可以实现各种强大的网络应用。本文将深入探讨 Linux TCP 编程的各个方面&…

Elasticsearch 开放推理 API 增加了 Azure AI Studio 支持

作者&#xff1a;来自 Elastic Mark Hoy Elasticsearch 开放推理 API 现已支持 Azure AI Studio。在此博客中了解如何将 Azure AI Studio 功能与 Elasticsearch 结合使用。 作为我们持续致力于为 Microsoft Azure 开发人员提供他们选择的工具的一部分&#xff0c;我们很高兴地宣…

提示工程:少样本提示(Few-shot Prompting)

少样本提示&#xff08;Few-shot Prompting&#xff09;是一种利用大语言模型从少量示例样本中学习并处理任务的方法。它的核心思想是利用大语言模型的上下文学习能力&#xff0c;通过在提示中增加“示例样本”来启发大语言模型达到举一反三的效果。这种方法避免了重新训练或者…

封装descriptions组件,描述,灵活

效果 1、组件1&#xff0c;dade-descriptions.vue <template><table><tbody><slot></slot></tbody> </table> </template><script> </script><style scoped>table {width: 100%;border-collapse: coll…

数据中台是什么?:架构演进、业务整合、方向演进

文章目录 1. 引言2. 数据中台的概念与沿革2.1 概念定义2.2 历史沿革 3. 数据中台的架构组成与关键技术要素解析3.1 架构组成3.2 关键技术要素 4. 数据中台与其他平台的对比详细解析 5. 综合案例&#xff1a;金融行业数据中台落地实践5.1 背景5.2 解决方案5.3 成果与价值 6. 方向…

【Matlab优化算法-第14期】基于智能优化算法的VMD信号去噪项目实践

基于智能优化算法的VMD信号去噪项目实践 一、前言 在信号处理领域&#xff0c;噪声去除是一个关键问题&#xff0c;尤其是在处理含有高斯白噪声的复杂信号时。变分模态分解&#xff08;VMD&#xff09;作为一种新兴的信号分解方法&#xff0c;因其能够自适应地分解信号而受到…

ubuntu20使用tigervnc远程桌面配置记录

一、安装tigervnc sudo apt install tigervnc-common sudo apt install tigervnc-standalone-server二、增加配置文件 安装完后新增配置文件&#xff1a;vim ~/.vnc/xstartup #!/bin/sh #Uncomment the following two lines for normal desktop: #unset SESSION_MANAGER #ex…

【WB 深度学习实验管理】使用 PyTorch Lightning 实现高效的图像分类实验跟踪

本文使用到的 Jupyter Notebook 可在GitHub仓库002文件夹找到&#xff0c;别忘了给仓库点个小心心~~~ https://github.com/LFF8888/FF-Studio-Resources 在机器学习项目中&#xff0c;实验跟踪和结果可视化是至关重要的环节。无论是调整超参数、优化模型架构&#xff0c;还是监…

编译spring 6.2.2

如何编译Spring 6.2.2 下载spring 6.2.2 首先&#xff0c;下载spring 6.2.2&#xff0c;地址&#xff1a;下载 解压到你的目录下。 下载gradle 下载gradle&#xff0c;这是spring项目的依赖管理工具&#xff0c;本文下载的是8.12.1。 gradle idea配置如下&#xff1a;在你的…

【centOS】搭建公司内网git环境-GitLab 社区版(GitLab CE)

1. 安装必要的依赖 以 CentOS 7 系统为例&#xff0c;安装必要的依赖包&#xff1a; sudo yum install -y curl policycoreutils openssh-server openssh-clients postfix sudo systemctl start postfix sudo systemctl enable postfix2. 添加 GitLab 仓库 curl -sS https:/…

【R语言】plyr包和dplyr包

一、plyr包 plyr扩展包主要是实现数据处理中的“分割-应用-组合”&#xff08;split-apply-combine&#xff09;策略。此策略是指将一个问题分割成更容易操作的部分&#xff0c;再对每一部分进行独立的操作&#xff0c;最后将各部分的操作结果组合起来。 plyr扩展包中的主要函…

《XSS跨站脚本攻击》

一、XSS简介 XSS全称&#xff08;Cross Site Scripting&#xff09;跨站脚本攻击&#xff0c;为了避免和CSS层叠样式表名称冲突&#xff0c;所以改为了XSS&#xff0c;是最常见的Web应用程序安全漏洞之一&#xff0c;位于OWASP top 10 2013/2017年度分别为第三名和第七名&…