ThreadLocal与InheritableThreadLocal及线程池的影响

在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 {@Overridepublic void run() {System.out.println("[FirstTask] print ThreadLocal: " + fatherThread.get());System.out.println("[FirstTask] print InheritableThreadLocal:" + fatherInheritableThread.get());}}static class SecondTask implements Runnable {@Overridepublic 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();// 限制线程数为1threadPoolTaskExecutor.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{@Overridepublic 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.hqwc.cn/news/18445.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux 批量杀掉进程(包含某个关键字)

一、场景说明 现场环境有十多个包含 ”celery” 关键字的进程在运行&#xff0c;每次重启服务&#xff0c;需要将这些进行kill掉&#xff0c;然后重新启动。 可以用如下命令批量kill掉这些进程&#xff1a; kill -9 PID1 PID2 PID3 PID4.....其中&#xff0c;PID是查询到的进…

Node.js开发

Node.js是一个基于V8 JavaScript引擎的JavaScript运行时环境。 也就是说Node.js基于V8引擎来执行JavaScript的代码&#xff0c;但是不仅仅只有V8引擎&#xff1a;  前面我们知道V8可以嵌入到任何C 应用程序中&#xff0c;无论是Chrome还是Node.js&#xff0c;事实上都是嵌入…

Docker NGINX 加载Geoip模板

前提环境&#xff1a; Docker 环境 涉及参考文档&#xff1a; ngx_http_geoip_module 模块Loki NGINX Service MeshGeoIP IP库 一、下载GeoIP IP库 二、配置Nginx主配置文件 vim /data/nginx/MangoMoh/dos/nginx.confuser nginx; worker_processes auto;error_log /var…

postman每次请求前获取token并保存到环境变量

文章目录 创建一个tab&#xff0c;写上接口地址&#xff0c;json类型的参数&#xff0c;点击send&#xff0c;返回成功。 点击Environment&#xff0c;新建dev环境&#xff0c;创建token变量 编写Tests脚本 // 获取响应的token值 var responseJson pm.response?.json()…

【Redis】特殊数据类型 Geo (地理位置)

&#x1f3af;前言 除了五中基本的数据类型外&#xff0c;Redis还支持两种特殊的数据类型&#xff0c;第一种 Geo (地理位置)&#xff1a;用于存储地理位置相关的数据&#xff0c;例如经纬度、距离等。第二种 Stream (流)&#xff1a;是一个高级的列表类型&#xff0c;支持对列…

关于c/c++中的isdigit()函数(判断一个字符是不是数字字符)

1&#xff1a;做用&#xff1a;判断一个字符是不是数字字符&#xff08;即&#xff1a;相当于&#xff1a;s[i]>0&&s[i]<9&#xff09; 2&#xff1a;使用方式 char cA; string s"123fgv"; if(isdigit(c)); if(isdigit(s[i]))//返回bool类型 3&…

What Is the Character Limit for ChatGPT? 聊天GPT的字符限制是多少?

The character limit for ChatGPT text prompts is 4096 characters, and there’s also a limit of 4096 tokens per conversation. If you aren’t sure how many tokens your prompt uses, you can calculate that with OpenAI’s Tokenizer tool. ChatGPT 文本提示的字符限…

每周学点数学 3:概率论基础2

文章目录 1.独立性与相关性2.条件概率与边缘概率3.大数定律与中心极限定理4.随机过程5.概率论的应用 1.独立性与相关性 独立性与相关性是在数据分析中非常重要的两个概念&#xff0c;它们之间存在一定的联系&#xff0c;但也有明显的区别。 独立性&#xff08;Independence&…

Vue+element实现el-table行内编辑并校验

el-table行内编辑情况情况概要&#xff1a;之前在开发过程中对于element数据的新增&#xff0c;修改&#xff0c;删除。一般直接结合el-form使用。也就是新增的时候点新增然后出来一个弹框&#xff0c;里面嵌套一个表单&#xff0c;然后保存就好了。这次项目中要求所有的新增&a…

AppSpider Pro 7.4.053 for Windows - Web 应用程序安全测试

AppSpider Pro 7.4.053 for Windows - Web 应用程序安全测试 Rapid7 Dynamic Application Security Testing (DAST) 请访问原文链接&#xff1a;https://sysin.org/blog/appspider/&#xff0c;查看最新版。原创作品&#xff0c;转载请保留出处。 作者主页&#xff1a;sysin…

【软件测试】Git 远程仓库的使用(详细)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 查看远程仓库 想…

Web入门-HTTP协议

目录 HTTP概述 HTTP特点 HTTP请求协议 请求数据的格式 响应数据的格式 响应的状态码 HTTP协议的解析 HTTP概述 HTTP:Hyper Text Transfer Protocol&#xff0c;超文本传输协议&#xff0c;规定浏览器和服务器之间数据传输的规则。(即请求数据和响应数据的格式)以上一篇…