ThreadLocal与InheritableThreadLocal及线程池的影响

news2024/11/25 14:45:11

在web开发中使用了ThreadLocal本地线程存储拦截器解析的用户信息,方便在下文代码中调用,但是在springboot中使用@Async开启异步操作时,就会造成,子线程无法拿到父本地线程数据。拿到一些脏数据。

1.InheritableThreadLocal

  • 在这个问题上,java为我们提供了InheritableThreadLocal类,该类是继承了ThreadLocal,它具有与普通ThreadLocal相同的功能,但还具有一个额外的特性,即可以在子线程中继承父线程的变量值。
  • 普通的ThreadLocal变量只是在每个线程内部创建副本,而InheritableThreadLocal可以将父线程的变量值传递给子线程。这对于需要跨线程传递上下文信息或者共享线程间状态的场景非常有用。
private static ThreadLocal<String> fatherInheritableThread = new InheritableThreadLocal<>();

探讨

使用InheritableThreadLocal类可以实现父子线程的共享,通过源码发现,我们当前线程A里正在创建子线程A-1,A线程去判断自己的inheritableThreadLocals是否有值,有值返回给当前正在创建的A-1的inheritableThreadLocals属性。
注意,这里代码有点绕,一看咋是自己赋值给自己,但仔细来看, Thread parent = currentThread();parent是当前正在new Thread()的线程(正在创造A-1),是A-1线程拿到了当前正在运行自己的A线程的inheritableThreadLocals然后this.inheritableThreadLocals = inheritableThreadLocals赋值给了自己。this当然就是A-1了。A线程是父线程其数据当然是我们主动set的了。到此,一个新的子线程A-1诞生了。
在这里插入图片描述
子线程A-1创建完后,InheritableThreadLocal.get()获取数据,get方法没有被重写,其位于父类ThreadLocal里面。
在这里插入图片描述
InheritableThreadLocal重写getMap,使之在get方法时,获取this.InheritableThreadLocals的数据,
在这里插入图片描述
在构造ThreadLocalMap时,调用了InheritableThreadLocal重写的childValue,直接返回父线程值。
在这里插入图片描述
容易混淆的是,在这里,我们操作的是inheritableThreadLocals属性,可以理解为对于一个线程,threadLocals与inheritableThreadLocals是平级关系。

问题

也就是说,只有当子线程被new的时候,父子线程才会同步数据,对于使用线程池模式线程复用的情况下,就会出现多线程问题,即A线程的value=5,A线程任务结束后,执行B任务,A线程至始至终没有被销毁,所以B任务始终无法获取父线程最新的数据,从而导致污染。InheritableThreadLocal并没有改变Thread类的行为,而是在Thread类中新增了一个成员变量来存储线程的变量副本。它利用了Java的继承机制,在子线程创建时复制父线程的变量副本,实现了父线程变量传递给子线程的功能。

当创建一个新的子线程时,Java会调用Thread类的init() 方法来初始化该线程。在init()方法中,会通过inheritableThreadLocals的createInheritedMap(ThreadLocalMap parentMap)方法来创建一个新的InheritableThreadLocalMap对象,并将父线程的inheritableThreadLocals中的值复制到新的InheritableThreadLocalMap对象中。如果线程被复用,则不会调用init方法从而没有以上效应。

线程池线程复用实例:

package com.liubingzhe.smartFile;

import org.springframework.core.task.TaskDecorator;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;


public class ThreadTest {

    private static ThreadLocal<String> fatherThread = new ThreadLocal<>();
    private static ThreadLocal<String> fatherInheritableThread = new InheritableThreadLocal<>();

    static class FirstTask implements Runnable {
        @Override
        public void run() {
            System.out.println("[FirstTask] print ThreadLocal: " + fatherThread.get());
            System.out.println("[FirstTask] print InheritableThreadLocal:" + fatherInheritableThread.get());
        }
    }

    static class SecondTask implements Runnable {
        @Override
        public void run() {
            System.out.println("[SecondTask] print ThreadLocal: " + fatherThread.get());
            System.out.println("[SecondTask] print InheritableThreadLocal:" + fatherInheritableThread.get());
        }
    }


    public static void main(String[] args) throws InterruptedException {
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        // 限制线程数为1
        threadPoolTaskExecutor.setMaxPoolSize(1);
        threadPoolTaskExecutor.setCorePoolSize(1);
        threadPoolTaskExecutor.setQueueCapacity(1);
        threadPoolTaskExecutor.setThreadNamePrefix("async-");
        threadPoolTaskExecutor.initialize();
        String printText = "this is father thread print";
        fatherInheritableThread.set(printText);
        fatherThread.set(printText);
        System.out.println("父线程初始数据");
        System.out.println("fatherThread print: " + fatherThread.get());
        System.out.println("fatherInheritableThread print: " + fatherInheritableThread.get());
        System.out.println("子线程1开始执行");
        threadPoolTaskExecutor.execute(new FirstTask());
        printText = "change text";
        fatherInheritableThread.set(printText);
        Thread.sleep(2000);
        System.out.println("子线程2开始执行");
        threadPoolTaskExecutor.execute(new SecondTask());

    }
}

我们限制线程池仅允许一个线程使之复用,并执行这两个异步,中途我们修改printText的值,观察结果:

父线程初始数据
fatherThread print: this is father thread print
fatherInheritableThread print: this is father thread print
子线程1开始执行
[FirstTask] print ThreadLocal: null
[FirstTask] print InheritableThreadLocal:this is father thread print
子线程2开始执行
[SecondTask] print ThreadLocal: null
[SecondTask] print InheritableThreadLocal:this is father thread print

可以发现子线程2并没有发生改变。通过在子线程中增加 System.out.println(Thread.currentThread().getId());可以进一步确定是一个线程在运行,当然也只能是一个。

怎么解决?

  1. TransmittableThreadLocal可以解决
  2. 使用线程池装饰器threadPoolTaskExecutor.setTaskDecorator(new myDecorator());
  • 在线程运行前,获取当前父线程上下文的值
  • 执行前给当前任务创建一个新的本地线程,并将父线程上下文数据赋值给当前新的本地线程,然后赋值给父线程(本质创建了一个父线程副本)
    static class myDecorator implements TaskDecorator{

        @Override
        public Runnable decorate(Runnable runnable) {
            String fatherValue = fatherInheritableThread.get();
            return ()->{
                InheritableThreadLocal<String> objectThreadLocal = new InheritableThreadLocal<>();
                try{
                    objectThreadLocal.set(fatherValue);
                    fatherThread = objectThreadLocal;
                    runnable.run();
                }finally {
                    objectThreadLocal.remove();
                }

            };
        }
    }

在一个线程情况下的打印结果,数据发生改变

父线程初始数据
fatherThread print: this is father thread print
fatherInheritableThread print: this is father thread print
子线程1开始执行
12
[FirstTask] print ThreadLocal: this is father thread print
[FirstTask] print InheritableThreadLocal:this is father thread print
子线程2开始执行
12
[SecondTask] print ThreadLocal: change text
[SecondTask] print InheritableThreadLocal:this is father thread print

第二种方法是我在开发过程中临时想到了一个解决方法,可能存在一些问题,但是后来通过来了解TransmittableThreadLocal类发现,基本思路一致,都是创建了线程副本,但是其是通过在切换线程时,还原了线程状态。

TransmittableThreadLocal是一个第三方库,它通过字节码增强和拦截线程切换的方式实现了在线程之间传递上下文的功能。它是基于InheritableThreadLocal的变体,并且专门用于解决在线程池等场景下的线程传递问题。

下面是TransmittableThreadLocal的基本原理:

字节码增强:TransmittableThreadLocal使用字节码增强技术,在运行时修改Java字节码。在线程类(Thread)和线程池类(ThreadPoolExecutor)的相关方法中,插入了额外的代码。

线程切换的拦截:TransmittableThreadLocal使用ThreadLocal类的特性,通过继承ThreadLocal并重写相关方法来实现对线程切换的拦截。具体是重写ThreadLocalbeforeExecute()afterExecute()remove()方法,这些方法会在线程池中执行任务前后被调用,以及从线程池中移除线程时被调用。

状态保存与恢复:在线程切换之前,TransmittableThreadLocal会保存当前线程中所有TransmittableThreadLocal对象的状态或值。保存的方式是通过自动化的字节码增强操作,将相关状态存储到一个共享的数据结构中。

线程切换时的绑定与解绑操作:在线程切换之后,TransmittableThreadLocal会在新线程中恢复之前保存的值。它会根据线程的标识符(Thread ID)来获取对应的之前保存的状态。这样,新线程就能够访问到与之前线程相关的TransmittableThreadLocal对象的值。

总的来说,TransmittableThreadLocal通过使用字节码增强和拦截线程切换的方式,实现了在线程之间传递上下文的能力。通过保存和恢复线程中的TransmittableThreadLocal的状态,它确保在线程池等复杂场景下,正确地传递变量值,保持线程之间的隔离性

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

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

相关文章

常用技巧总结

本文总结了本人在日常工作学习中遇到的问题及其解决方法&#xff0c;没有固定的涉及领域 目的就是为了在下一次遇到类似问题的时候方便查找&#xff0c;从而快速解决问题 本文不定时更新~ 目录 Windows使用 如何实现桌面图标随意排列 文件资源管理器相关 显示隐藏文件 修改…

深度学习06-深度卷积生成对抗网络(DCGAN)

文章目录 概述原理简介专业术语零填充转置卷积 生成动漫图像算力选择数据集目录规划源代码数据源加载定义配置类定义模型 训练可视化绘制损失绘制生成器图像变化 其他项目CycleGANstargan 概述 GAN&#xff08;Generative Adversarial Network&#xff09;是一种生成模型&…

flutter开发实战-日志logger写入文件及print

flutter开发实战-日志logger写入文件及print 在开发中&#xff0c;需要日志logger写入文件&#xff0c;方便日后查看出现的问题。这里记录之前的实现方案。 使用的日志插件是logger 一、引入日志插件 在工程中pubspec.yaml引入logger logger: ^1.4.0二、代码实现 使用比较…

听GPT 讲K8s源代码--pkg(一)

在 Kubernetes 代码仓库中&#xff0c;pkg/api和pkg/apis目录都包含用于定义 Kubernetes API 对象的代码&#xff0c;但它们的作用略有不同。 pkg/api目录包含 Kubernetes 的旧版本 API 对象定义&#xff0c;这些定义在 Kubernetes 1.7 版本之前使用。这些对象定义已经过时&…

第四课:Figma 图标设计

图形绘制小技巧 绘制正圆 O shift 正方体 R shift&#xff0c;右侧属性面板可调整描边显示方向&#xff0c;描边类型&#xff1b; 直线 L shift 带角度的箭头 shift L &#xff0c;按住 shift 键调整以 45 度角调整箭头方向&#xff0c;右侧属性面板可调节箭头方向和线条…

文献里的分子对接方法2

珍珠贝肉水解物中的新型抗氧化肽以及它们的抗氧化活性机制。 关于摘要&#xff1a; 自由基和衰老以及很多疾病都有关联。 抗氧化肽具有良好的抗氧化活性和吸收性&#xff0c;是抗氧化剂研究的热点之一。 这篇文献的研究中&#xff0c;纯化珍珠贝肉水解液、经过蛋白质组学鉴定…

大模型与端到端会成为城市自动驾驶新范式吗?

摘要&#xff1a; 最近可以明显看到或者感受到第一梯队的城市自动驾驶量产已经进入快车道&#xff0c;他们背后所依靠的正是当下最热的大模型和端到端的技术。 近期&#xff0c;城市自动驾驶量产在产品和技术上都出现了新的变化。 在产品层面&#xff0c;出现了记性行车或者称…

【python】逻辑中断(and or)

今天学习javascript的时候竟然有一个额外收获&#xff1a;逻辑中断。而且我实验了一下&#xff0c;逻辑中断同样适用于python。 0 and 2返回&#xff1a; 0 1 and 2返回&#xff1a; 2 0 and 2返回的是0&#xff0c;而1 and 2返回的是2。就是因为在0那里出现了逻辑中断。 解…

Git的使用以及在IDEA2022中使用Git

目录 前言 一、Git下载与安装 二、Git常用命令 1.全局设置 2.获取Git仓库-在本地初始化Git仓库 3.获取Git 仓库-从远程仓库克隆 4.Git工作区、暂存区、版本库 概念以及工作区中文件状态 &#xff08;1&#xff09;工作区、暂存区、版本库 &#xff08;2&#xff09; G…

centos7.9 rc.local启动失败

最近在虚拟机上整个centos7.9&#xff0c;想要把程序设置成开机自启动&#xff0c;发现rc.local状态是failed&#xff0c;重启服务器也没用。 自己之前写过两篇相关的博客 centos7重启后/etc/rc.local中的脚本没有执行_lanren312的博客-CSDN博客 centos7开机运行java的sh脚本…

Sui 8192:如何使用Sui对象撼动游戏领域

Ethos通过其Sui 8192游戏不仅展示了对象在Sui上的力量&#xff0c;还展示了基于对象的游戏如何有力地撼动游戏行业。每个玩家在移动游戏中的方块时都会铸造一个新的对象&#xff0c;类似于铸造NFT&#xff0c;这为每个独立玩过的游戏提供了永久上链的机会。 Sui 8192游戏的规则…

10.4.4 终端机的环境设置: stty, set

在 tty1 ~ tty6 这六个命令行的终端机&#xff08;terminal&#xff09; 环境中登陆&#xff0c;登陆的时候我们可以取得一些字符设置的功能。举例来说&#xff0c;我们可以利用倒退键 &#xff08;backspace&#xff0c;就是那个←符号的按键&#xff09; 来删除命令列上的字符…

强化学习模型

目录 引言 1 强化学习的理论基础 2 强化学习的实践 3实战案例:自动驾驶

【InnoDB 存储引擎】InnoDB 数据页格式(详细版,数据页格式对于理解索引详细的原理很重要)

文章目录 1 InnoDB 数据页结构1.1 File Header2.2 Page Header2.3 Infimum 和 Supremum Record2.4 User Record 和 Free Space2.5 Page Directory&#xff08;InnoDB 数据页结构最重要的部分&#xff09;2.6 File Trailer 2 参考资料 1 InnoDB 数据页结构 我们已经知道页是 In…

PS 2023 24.7 Beta Ai 如何解决橙色错误弹窗问题?

距离Adobe软件公司首次将图像编辑及数字绘画软件Photoshop推出到大众面前已经过去35年&#xff0c;最近该公司又再次书写了属于Photoshop的历史新篇章。 Adobe 发布的 Photoshop&#xff08;Beta&#xff09;新增「创意填充&#xff08;Generative Fill&#xff09;」功能&…

jmter连接mysql数据库取值

测试情况下需要大量的测试数据进行模拟测试&#xff0c;如何使用接口插入大量数据&#xff0c;使用jmter进行插入 步骤如下&#xff1a; 第一步&#xff1a;\lib\ext导入mysql-connector-java-5.1.44-bin.jar驱动 F:\TOOLS\apache-jmeter-5.4.1\apache-jmeter-5.4.1\lib\ext …

Vue的入门学习

Vue 1 Vue概述 通过我们学习的htmlcssjs已经能够开发美观的页面了&#xff0c;但是开发的效率还有待提高&#xff0c;那么如何提高呢&#xff1f;我们先来分析下页面的组成。一个完整的html页面包括了视图和数据&#xff0c;数据是通过请求 从后台获取的&#xff0c;那么意味…

SpringBoot中集成阿里开源缓存访问框架JetCache实现声明式实例和方法缓存

场景 SpringBoot中通过自定义缓存注解(AOP切面拦截)实现数据库数据缓存到Redis&#xff1a; SpringBoot中通过自定义缓存注解(AOP切面拦截)实现数据库数据缓存到Redis_霸道流氓气质的博客-CSDN博客 上面讲的通过自定义注解的方式实现查询数据库数据缓存&#xff0c;除此之外…

数据结构--哈夫曼树

数据结构–哈夫曼树 带权路径长度 结点的 权 \color{red}权 权:有某种现实含义的数值&#xff08;如:表示结点的重要性等) 结点的带权路径长度 \color{red}结点的带权路径长度 结点的带权路径长度:从树的根到该结点的路径长度(经过的边数&#xff09;与该结点上权值的乘积 树的…

Element UI组件中el-col、el-row布局学习笔记

一、简介 el-col&#xff1a;列。是Element UI布局中的核心组件&#xff0c;他的作用的将一行分成24个网格&#xff0c;为了方便我们在不同的设备上适配不同的屏幕大小。我们可以通过指定span属性来确定一行中所占的网格数。 el-row&#xff1a;行。包裹在el-col外层&#xf…