一个BUG搞懂ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal

news2024/11/14 20:51:26

首发公众号:赵侠客

引言

最近我收到一个非常诡异的线上BUG,触发BUG的业务流程大概是这样的:A系统新建任务数据需要同步到B系统,数据是多租户的,比如C租户在A系统新建了一条任务,那么C租户登录B系统后会看到这条任务,如果在A系统修改这条任务,任务信息会也会同步到B系统,本来是一个很简单的数据同步问题,但是诡异的事情发生了。

二、问题排查

BUG现象:

  • C租户在A系统新建任务后,去B系统找不到这条任务
  • C租户在A系统修改任务后,去B系统又看到了这条任务

开发初步排查:

  • C租户在A系统新建任务后数据确实入了B系统的数据库,只是租户ID变成了D租户,所以在B系统中查不到这条任务
  • C租户在A系统修改任务后B系统的任务租户ID又被改成了租户C的,所以在B系统中又能查到这条任务

排查思路:

我收到这个问题时,另一个开发已经做了初步排查,说这个问题太诡异了,他找不出原因,于是请我帮忙排查一下,因为这个项目不是我开发的,业务也不是很熟悉,我的排查思路大致是这样的:

  1. 最近有没有发过版

我找到运维看线上包是6月17号发的版,而出现问题是从7月23号,所以不太可能是发版导致的问题

  1. 诡异的租户ID

因为新增任务租户ID被改成固定值25678,我在代码及配置中搜索了这个关键字,没有搜索到,然后怀疑有人改了代码未合并Master

  1. 线上代码反编译

为了排除是代码不一致导致的问题,我反编译了一下线上环境的代码,发现和Master是一样的,所以排除了代码不一致的情况,也排除了代码中有固定用租户ID的情况

  1. 查看代码

代码其实是比较简单的,我省略其它不必要的逻辑,主要功能就是接收一个对象,然后使用taskRepository.save()保存到数据库中,添加和修改都是调用这个接口,那为什么添加任务租户ID会被改,修改任务租户ID就不会被改呢?从代码初步看是没有问题的,好像可以排除代码的问题

 
@PostMapping("/api/sync")
public ResponseEntity<String> sync(@RequestBody TaskDO taskDO) {
        taskRepository.save(taskDO);
        return ResponseEntity.ok("ok");
}

public class TaskDO {
    private Long id;
    private String name;
    private Long tenantId;
}
  1. 线上Debug

然后我使用Arthas 在线上追踪了一个这个sync方法,在save()之前taskDO租户ID是正确的,然而save()之后taskDO的tenantId就被改掉了,这时我断定肯定是有拦截之类的东西修改了租户ID

  1. 重启大法

问题一时没找到,然后我使用了重启大法,试下重启后能不能解决问题,结果重启后真的就好了,这时可以肯定的是25678租户ID不是代码和配置里取的,肯定是从内存里取的,因为重启后内存里没有了这个数字,内存取不到也就好了

  1. 继续耐心看代码

重启大法确实好用,但是问题没有找到,说不定过段时间问题又会出现,于是我还是耐心去看代码,因为代码比较老也不是我写的,而且老代码你们都知道,基本上就是一座屎山,看起来还是非常要耐心的,当我看到代码里使用了ThreadLocal保存租户ID,这里我就知道是啥原因了,大概率是使用了ThreadLocal后没有清理,Tomcat处理请求使用了线程池复用导致的。

三、问题还原

这里我简化一下不必要的代码,大致复原一下核心代码,首先有一个UserContext使用了ThreadLocal保存TenantId

 
public class UserContext {
    private static  ThreadLocal<Long> userTenant=new ThreadLocal<>();

    public static void setTenantId(Long tenantId){
        userTenant.set(tenantId);
    }
    public static  Long getTenantId(){
        return userTenant.get();
    }
    public static void remove(){
        userTenant.remove();
    }
}

然后有一个获取当前租户任务的接口,这里的租户ID是从UserContext中获取的

 
@GetMapping("/task")
public ResponseEntity<List<TaskDO>> list() {
    log.info("GET /task use threadId: {}",Thread.currentThread().getId());
    return ResponseEntity.ok(taskRepository.findAllByTenantId(UserContext.getTenantId()));
}

另外有一个登录拦截器,大致逻辑是从请求Token里解析出tenantId然后设置到UserContext

 
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String tenantId = request.getHeader("X-TENANT-ID");
        if (tenantId != null) {
            UserContext.setTenantId(Long.parseLong(tenantId));
        }
        return true;
    }
}

在数据实体对象上有一个@EntityListeners

 
@Entity
@Data
@Table(name = "task")
@EntityListeners(value = { AddListener.class })
public class TaskDO {
    @Id
    private Long id;
    private String name;
    private Long tenantId;
}

在AddListener里使用 @PrePersist拦截了添加操作,将当前对象的tenantId设置成UserContext的租户ID

 
public class AddListener {
    @PrePersist
    public void preSetTenantId(Object entity) throws Exception {
        Long tenantId = UserContext.getTenantId();
        if (tenantId == null) {
            return;
        }
        Field tenantidField = entity.getClass().getDeclaredField("tenantId");
        if (tenantidField == null) {
            return;
        }
        tenantidField.setAccessible(true);
        tenantidField.set(entity, tenantId);
    }
}

为了更快的模拟出效果我们将Tomcat的最大线程数量设置为1



server:
  tomcat:
    threads:
      max: 1

然后先调用租户1的获取任务列表,再调用/api/sync,第一次调用/api/sync接口是新增任务的租户ID为1,第二次调用是修改操作租户ID被改成2,完全和线上的BUG一样。

 
###
GET localhost/task
X-TENANT-ID: 1


###
POST localhost/api/sync
Content-Type: application/json

{
  "id": 8,
  "name": "赵侠客任务8",
  "tenantId": 2
}

四、** **ThreadLocal总结

其实ThreadLocal并不是什么很高明的设计,它只是对Thread对象中一个Map成员变量的封装,说白了你完全可以在Thread对象中定义一个Map,然后通过Thread.currentThread().getMap()来获取这个Map,然后直接通过map.put()保存当前线程的数据,也能达到ThreadLocal一样的效果,而且使用起来更简单方便。我们可以简单看下ThreadLocal的实现:

ThreadLocal的set()方法:

ThreadLocal的set()方法主要通过以下三步:

  • 通过Thread.currentThread()获取当前线程对象

  • 通过 getMap(t)获取当前线程对象中的ThreadLocalMap

  • 将当前ThreadLocal对象当作Key,要设置的value当作值添加到map中

 
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
}

ThreadLocal的getMap()方法

getMap()方法直接返回了Thread对象中的threadLocals, 如果map对象是空会调用createMap()方法将Thread对象的中ThreadLocalMap变量创建一个新的对象

 
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
    

ThreadLocalMap对象

ThreadLocalMap对象并不是JAVA中的Map,而是ThreadLocal中定义的一个简单Map,使用Entry存储MAP中的数据,这里值得注意的是Entry继承了WeakReference是不个弱引用。

弱引用:弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了「只具有弱引用」的对象,不管当前内存空间足够与否,都会回收它的内存

关于这里为什么要使用弱引用,主要是因为使用强应用会导致Entry对象一直不被回收从而产生内存泄露,具体原因网上有很多文章详细分析了,有兴趣可以搜下ThreadLocal为什么使用弱引用

 
static class ThreadLocalMap {
    
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
}

ThreadLocal对象原理是非常简单的,使用后一定要即使的清理,本次BUG解决方法就是在请求结束后调用UserContext.remove()清理当前线程中的保存ThreadLocal对象中的值就好了

 
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserContext.remove();
    }
    
}

五、InheritableThreadLocal

在Thread对象中除了有一个 ThreadLocal.ThreadLocalMap threadLocals对象外还有一个成员变量 ThreadLocal.ThreadLocalMap inheritableThreadLocals,操作它对应的封装类叫InheritableThreadLocal

 
ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

InheritableThreadLocal是继承了是ThreadLocal,只是在getMap()是返回了Thread对象中的inheritableThreadLocals,在createMap()时将ThreadLocalMap对象符给Thread对象中的 inheritableThreadLocals成员变量。

 
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    public InheritableThreadLocal() {}
    protected T childValue(T parentValue) {
        return parentValue;
    }
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

InheritableThreadLocal和ThreadLocal的区别就是InheritableThreadLocal在子线程中可以获取到主线程中的值,我们看下面的Demo

 
public class TestThreadLocal implements Runnable {
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    private static ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();

    public static void main(String[] args) {
        threadLocal.set("公众号");
        inheritableThreadLocal.set("赵侠客");
        Thread tt = new Thread(new TestThreadLocal());
        tt.start();
    }
    @Override
    public void run() {
        System.out.println("子线程中的值threadLocal:" + threadLocal.get());
        System.out.println("子线程中的值inheritableThreadLocal:" + inheritableThreadLocal.get());
    }
}

▲子线程可获取主线程中的变量

从图中可以看出在子线程中可以通过InheritableThreadLocal获取主线程中的值,但是ThreadLocal获取不到的

InheritableThreadLocal原理

我们看Thread对象的构造方法里有一段如下代码:

 
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
    this.inheritableThreadLocals =
        ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

在创建线程时,如果父级线程的parent.inheritableThreadLocals不为空,则将父级线程中的inheritableThreadLocals给当前线程,也就是说使用InheritableThreadLocal只能在创建线程时同步父级线程中的值,后面父级线中的值修改是不会同步到子线程的。

我们看下面的代码:在创建子线程后我们在主线程里将inheritableThreadLocal中的值修改

 
public class TestThreadLocal implements Runnable {
    private static ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
    public static void main(String[] args) throws InterruptedException {
        inheritableThreadLocal.set("公众号-赵侠客");
        Thread tt = new Thread(new TestThreadLocal());
        tt.start();
        inheritableThreadLocal.set("掘金-赵侠客");
        System.out.println("主线程inheritableThreadLocal:"+inheritableThreadLocal.get());
        tt.join();
    }
    @Override
    public void run() {
        System.out.println("子线程inheritableThreadLocal:" + inheritableThreadLocal.get());
    }
}

在这里插入图片描述

▲主线程修改变量子线程不会更新

可以看出主线程中修改了InheritableThreadLocal中的值,在子线程是不会更新的,获取的还是老的值。

那么有没有什么破解之法呢?当然有了,这时就轮到我们TransmittableThreadLocal登场了,TransmittableThreadLocal是阿里开源的一个框架指在解决InheritableThreadLocal主线程对象修改无法同步子线程的问题

六、TransmittableThreadLocal

官网地址:https://github.com/alibaba/transmittable-thread-local

 
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.12.6</version>
</dependency>

使用方法:

 
public class TestThreadLocal implements Runnable {

    public static ExecutorService executorService = Executors.newFixedThreadPool(1);

    private static TransmittableThreadLocal<String> transmittableThreadLocal = new TransmittableThreadLocal<>();

    public static void main(String[] args) {
        transmittableThreadLocal.set("公众号-赵侠客");
        Runnable task = new TestThreadLocal();
        //首次提交任务
        executorService.submit(TtlRunnable.get(task));
        //主线程修改值后需要再次提交任务
        transmittableThreadLocal.set("掘金-赵侠客");
        executorService.submit(TtlRunnable.get(task));
    }
    @Override
    public void run() {
        System.out.println("子线程transmittableThreadLocal:" + transmittableThreadLocal.get());
    }
}

在这里插入图片描述

▲主线程修改变量子线程中可以获取到更新后的值

可以看出子线程中成功获取到了主线程中修改后的值。

总结

本文从排查一个线程的BUG总结了ThreadLocal的基本用法主注意事项并引出了InheritableThreadLocal和TransmittableThreadLocal,针对这三个类可以做以下总结:

  • ThreadLocal中的变量作用域为当前线程,解决了多线程并发问题
  • ThreadLocal中的变量使用后要及时清理
  • ThreadLocal中Map对象是Key是自己,值为需要保存的对象
  • ThreadLocal子线程无法获取主线程中的值
  • InheritableThreadLocal 解决了子线程中获取主线程值的问题
  • InheritableThreadLocal 在主线程中修改变量后,子线程不会同步
  • TransmittableThreadLocal 解决了线程池复用时主线程变量修改同步子线程的问题

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

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

相关文章

基于springboot和vue的酒店管理系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图详细视频演示技术栈系统测试为什么选择我官方认证闲鱼玩家&#xff0c;服务很多代码文档&#xff0c;百分百好评&#xff0c;战绩可查&#xff01;&#xff01;入职于互联网大厂&#xff0c;可以交流&#xff0c;共同进步。有保障的售后 代码参考数据…

《黑神话.悟空》:一场跨越神话与现实的深度探索

《黑神话.悟空》&#xff1a;一场跨越神话与现实的深度探索 在国产游戏日益崛起的今天&#xff0c;《黑神话.悟空》以其独特的剧情、丰富的人物设定和深刻的主题&#xff0c;成为了无数玩家翘首以盼的国产3A大作。这款游戏不仅是一次对传统故事的创新演绎&#xff0c;更是一场对…

oracle日常巡检命令

一、日常巡检命令 1、检查Oracle实例状态 SQL> set pages 600 lines 600 SQL> select instance_name,host_name,startup_time,status,database_status from v$instance; 说明&#xff1a;“STATUS”表示Oracle当前的实例状态&#xff0c;必须为“OPEN”&#xff1b;“…

中国软件评测中心:2024最新人工智能大语言模型技术发展研究报告 (附文档)

人工智能作为引领新一轮科技产业革命的战略性技术和新质生产力重要驱动力&#xff0c;正在引发经济、社会、文化等领域的变革和重塑&#xff0c;2023 年以来&#xff0c;以 ChatGPT、GPT-4 为代表的大模型技术的出台&#xff0c;因其强大的内容生成及多轮对话能力&#xff0c;引…

swift微调款框架使用自定义数据集进行通义千问1.5的微调

使用自定义数据集进行通义千问1.5的 Swift 微调 模型训练手册文档 通义千问&#xff08;T2IQA&#xff09;是一个基于Transformer架构的问答系统&#xff0c;本文将介绍如何使用自定义数据集对Swift语言版本的通义千问进行微调&#xff0c;以适应特定的问题和领域。 swift微…

ubuntu server 扩容

环境&#xff1a;VirtualBox、Ubuntu-server 调整虚拟磁盘大小 在 VirtualBox 主界面 工具 -- 介质 中选择你要操作的虚拟磁盘&#xff0c;点击属性&#xff0c;更改大小即可&#xff0c;保存后启动虚拟机 查看磁盘状态 lsblk 可以看到 sda 已经是 128G 了。ubuntu--vg-ubun…

Vue下载文件的两种方法以及文件流处理

点击按钮下载文件 1.文件流形式 pdfHeaders: {Authorization: localStorage.getItem(Access-Token).replace(/"/g, ),Content-Type: application/json,}, downLoad(){let getUrl if (process.env.NODE_ENV "development") {getUrl 测试地址} else if (p…

浅探空间智能

空间智能&#xff0c;这一概念在人工智能领域逐渐升温&#xff0c;部分归功于AI界的领军人物李飞飞博士所领导的创新项目。 Seeing is for doing and learning. 【精校】TED&#xff1a;李飞飞 | 空间智能让AI理解真实世界 2024.5 李飞飞在 X 上介绍称&#xff0c;「空间智能…

【流媒体】基于libRTMP的H264推流器

目录 1. 整体流程2. 代码2.1 头文件2.2 c文件 3. 测试 RTMP协议相关&#xff1a; 【流媒体】RTMP协议概述 【流媒体】RTMP协议的数据格式 【流媒体】RTMP协议的消息类型 【流媒体】RTMPDump—主流程简单分析 【流媒体】RTMPDump—RTMP_Connect函数&#xff08;握手、网络连接&a…

智云-一个抓取web流量的轻量级蜜罐docker一键启动

智云-一个抓取web流量的轻量级蜜罐docker安装教程 github地址 https://github.com/xiaoxiaoranxxx/POT-ZHIYUN docker快速启动(v1.4) git clone https://github.com/xiaoxiaoranxxx/POT-ZHIYUN.git cd POT-ZHIYUN docker-compose up -d默认映射到80和8080端口 mysql不对外开放…

leetcode67. 二进制求和,简单模拟

leetcode67. 二进制求和 给你两个二进制字符串 a 和 b &#xff0c;以二进制字符串的形式返回它们的和。 示例 1&#xff1a; 输入:a “11”, b “1” 输出&#xff1a;“100” 示例 2&#xff1a; 输入&#xff1a;a “1010”, b “1011” 输出&#xff1a;“10101” …

【Java】 力扣 最大子数组和

目录 题目链接题目描述思路代码 题目链接 53.最大子数组和 题目描述 思路 动态规划解析&#xff1a; 状态定义&#xff1a; 设动态规划列表 dp &#xff0c;dp[i] 代表以元素 nums[i] 为结尾的连续子数组最大和。 为何定义最大和 dp[i] 中必须包含元素 nums[i] &#xff1a;…

一款免费的文件锁定占用解除工具,绿色免安装版

IObit Unlocker是一款由IObit公司开发的免费文件解锁工具&#xff0c;旨在解决用户在删除、重命名、移动或复制文件和文件夹时遇到的“无法删除”或“访问被拒绝”的问题。该软件体积小巧&#xff0c;不到3MB&#xff0c;非常易于使用&#xff0c;并且不需要安装&#xff0c;可…

【现代操作系统】3. 中断、异常、系统调用

通用概念 中断&#xff08;Interrupt&#xff09; 外部硬件设备所产生的信号异步&#xff1a;产生原因和当前执行指令无关&#xff0c;如程序被磁盘读打断 异常&#xff08;Exception&#xff09; 软件的程序执行而产生的事件包括系统调用同步&#xff1a;产生和当前执行或试图…

【AI学习】LLaMA模型的微调成本有几何?

在前面文章《LLaMA 系列模型的进化&#xff08;二&#xff09;》中提到了Stanford Alpaca模型。 Stanford Alpaca 基于LLaMA (7B) 进行微调&#xff0c;通过使用 Self-Instruct 方法借助大语言模型进行自动化的指令生成&#xff0c;Stanford Alpaca 生成了 52K 条指令遵循样例数…

【数据结构与算法】穷举搜索

穷举搜索目录 一.穷举搜索的原理二.穷举问题的引入三.穷举搜索的实现四.穷举搜索的高效版 一.穷举搜索的原理 列出所有可能出现的情况,逐个判断有那些是符合问题要求的条件. 通常可以从两方面分析: 问题所涉及的情况答案需要满足的条件 二.穷举问题的引入 有20枚硬币&#…

电价预测 | TSOA-TCN-Attention凌日算法优化时序卷积神经网络电价预测

目录 效果一览基本介绍程序设计 效果一览 基本介绍 电价预测 | TSOA-TCN-Attention凌日算法优化时序卷积神经网络电价预测 电价预测需求&#xff1a;随着能源市场的开放和电力交易的增加&#xff0c;准确的电价预测对于市场参与者的决策至关重要。而时序数据中的规律和趋势对于…

中小型制造企业质量管理设计与实现

文章目录 前言具体实现截图详细视频演示技术栈系统测试为什么选择我官方认证玩家&#xff0c;服务很多代码文档&#xff0c;百分百好评&#xff0c;战绩可查&#xff01;&#xff01;入职于互联网大厂&#xff0c;可以交流&#xff0c;共同进步。有保障的售后 代码参考数据库参…

多线程并行

多线程并行、所有线程结束后输出任务完成 示例 package com.fd;import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger;public class Test3 {public static void main(String[] args) throws InterruptedException {AtomicInteger counter…

【数据结构入门】二叉树之堆的实现

文章目录 前言一、树1.1 树的概念1.2 树的相关概念 二、二叉树2.1 二叉树的概念2.2 特殊的二叉树2.3 二叉树的性质 三、堆3.1 堆的概念3.2 堆的性质3.3 堆的存储3.4 堆的实现3.4.1 堆的初始化3.4.2 堆的销毁3.4.1 堆向上调整算法3.4.2 堆向下调整算法3.4.3 堆的创建3.4.4 堆的插…