解决:InheritableThreadLocal与线程池共用的问题

回顾一下上篇文章:InheritableThreadLocal和ThreadLocal的区别和使用场景
上篇文章介绍道,InheritableThreadLocal 是 ThreadLocal 的一个子类,它不但继承了ThreadLocal的所有特性,父线程中的 InheritableThreadLocal 变量的值可以被子线程继承。

为什么InheritableThreadLocal不能被线程池继承

上篇文章的最后留下一个引申思考,当InheritableThreadLocal 与线程池共用时,父线程中的 InheritableThreadLocal 变量的值能否被线程池中的工作线程继承?
答案是否定的,InheritableThreadLocal能够实现继承的源代码是存在于Thread类的构造器中,也就是说在父线程中new出来子线程才会实现InheritableThreadLocal继承。我们都知道线程池的特点是线程复用,线程池中的核心工作线程一旦创建,将长时间存在于线程池中。所以父线程使用线程池来分解任务时,并不会新建子线程,而是复用已有的线程,就不会触发Thread类的构造器的代码,导致父线程中的 InheritableThreadLocal 变量的值不能被线程池中的工作线程继承。

代码复现InheritableThreadLocal 与线程池共用

这里案例写的有点冗长,我想清楚还原线程池的实际运营状况。
大体思路:

  1. 先用高耗时的多个任务将线程池的核心线程数打满,确认刚开始线程池是能够继承InheritableThreadLocal的。
  2. 核心线程满了之后,修改父线程的InheritableThreadLocal变量
  3. 再提交新任务给线程池,确认线程池开始复用线程后就不能继承InheritableThreadLocal
public class ThreadLocalExample {// 创建一个 InheritableThreadLocal 变量private static final ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();// 创建一个4个工作线程的线程池private static ThreadPoolExecutor threadPoolExecutor  =new ThreadPoolExecutor(4, 4, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000));private static final AtomicInteger threadCount = new AtomicInteger();public static void main(String[] args) {// 设置主线程的 ThreadLocal 变量threadLocal.set("Thread main");// 标记一下工作线程的名字threadPoolExecutor.setThreadFactory(r -> {Thread thread = new Thread(r);thread.setName("WorkerThread-" + threadCount.incrementAndGet());return thread;});// 先写一个耗时任务将线程池打满for (int i = 0; i < 4; i++) {Thread thread = new Thread(() -> {try {Thread.sleep(1000);// 打印当前线程的 ThreadLocal 变量System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());} catch (InterruptedException e) {e.printStackTrace();}});// 将任务提交到线程池threadPoolExecutor.execute(thread);}// 等待线程池中的任务执行完成try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}// 确认一下线程池中的线程数System.out.println("线程池中的线程数:" + threadPoolExecutor.getPoolSize());// 再将主线程的 ThreadLocal 变量设置为 Thread Not MainthreadLocal.set("Thread not main");System.out.println("线程池中的线程已满,父线程的threadLocal值已修改为"+threadLocal.get()+",再提交一个任务到线程池中,查看子线程的threadLocal值是否会发生变化");// 再提交一个任务Thread thread = new Thread(() -> {try {Thread.sleep(1000);// 打印当前线程的 ThreadLocal 变量System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get());} catch (InterruptedException e) {e.printStackTrace();}});// 将任务提交到线程池threadPoolExecutor.execute(thread);// 关闭线程池threadPoolExecutor.shutdown();}
}

执行结果:

WorkerThread-2: Thread main
WorkerThread-1: Thread main
WorkerThread-4: Thread main
WorkerThread-3: Thread main
线程池中的线程数:4
线程池中的线程已满,父线程的threadLocal值已修改为Thread not main,再提交一个任务到线程池中,查看子线程的threadLocal值是否会发生变化
WorkerThread-2: Thread main

解决方法

方法一:

避免InheritableThreadLocal与线程池共用
不要吐槽这个解决方式,实际上这是一个最安全有效的方案

方法二:

不要在线程池的工作线程中获取InheritableThreadLocal参数值,而是采用普通参数传递方式传值
让我们将上文的代码做点改造,来证明使用普通参数传递方式传值是可信的。创建一个函数printThreadLocal分别打印线程名、ThreadLocal变量值和用过传参的方式传递的ThreadLocal变量值,利用函数式编程将该方法作为子任务交给线程池处理,如下:

public class ThreadLocalExample{// 创建一个 InheritableThreadLocal 变量private static final ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();// 创建一个4个工作线程的线程池private static ThreadPoolExecutor threadPoolExecutor  =new ThreadPoolExecutor(4, 4, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000));private static final AtomicInteger threadCount = new AtomicInteger();private static void printThreadLocal(String str) {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}// 打印当前线程的 ThreadLocal 变量System.out.println("当前线程:"+Thread.currentThread().getName()+";ThreadLocal变量" + threadLocal.get() + ";传递变量:" + str);}public static void main(String[] args) {// 标记一下工作线程的名字threadPoolExecutor.setThreadFactory(r -> {Thread thread = new Thread(r);thread.setName("WorkerThread-" + threadCount.incrementAndGet());return thread;});// 先写一个耗时任务将线程池打满for (int i = 0; i < 8; i++) {// 设置主线程的 ThreadLocal 变量threadLocal.set("Thread main"+i);// 在主线程中获取ThreadLocal变量的值String str = threadLocal.get();// 将任务提交到线程池,并以传参的方式传递ThreadLocal变量threadPoolExecutor.execute(()->printThreadLocal(str));}// 关闭线程池threadPoolExecutor.shutdown();}
}

执行结果

当前线程:WorkerThread-4;ThreadLocal变量Thread main3;传递变量:Thread main3
当前线程:WorkerThread-1;ThreadLocal变量Thread main0;传递变量:Thread main0
当前线程:WorkerThread-2;ThreadLocal变量Thread main1;传递变量:Thread main1
当前线程:WorkerThread-3;ThreadLocal变量Thread main2;传递变量:Thread main2
当前线程:WorkerThread-4;ThreadLocal变量Thread main3;传递变量:Thread main4
当前线程:WorkerThread-2;ThreadLocal变量Thread main1;传递变量:Thread main6
当前线程:WorkerThread-3;ThreadLocal变量Thread main2;传递变量:Thread main7
当前线程:WorkerThread-1;ThreadLocal变量Thread main0;传递变量:Thread main5

通过执行结果得知,当工作线程开始复用的时候,ThreadLocal变量就无法继承,但是通过参数传递的ThreadLocal变量一直是正确的。

方式三

本文的重点来了,各位都是优雅的程序设计师,怎么能够用以上的低级方式规避问题呢。我们必须优雅的解决InheritableThreadLocal与线程池共用的问题。
这里我给大家介绍一个线程池增强类ThreadPoolTaskExecutor ,这个类是Spring对ThreadPoolExecutor的加强,具体用法请参考我之前的文章这样用线程池才优雅-企业级线程池示例
在那个文章里,我在最后注释掉了一行配置:线程池的装饰器

//executor.setTaskDecorator(new ContextCopyingDecorator());

其实这个装饰器就可以解决InheritableThreadLocal与线程池共用的问题,翻开这个方法的doc,上面说到这个装饰器主要用于给线程任务设置一些上下文和监控,实际上相当于给工作任务做了一个切面。
是的,我们可以利用线程池装饰器(executor.setTaskDecorator())给工作线程做切面,这样就可以在任务执行之前将父线程的ThreadLocal赋值给工作线程,并能够在工作线程执行完毕后清除ThreadLocal,相当优雅。
下文将分享TaskDecorator实例(还没写)

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

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

相关文章

AI赋能写作:AI大模型高效写作一本通

❤️作者主页&#xff1a;小虚竹 ❤️作者简介&#xff1a;大家好,我是小虚竹。2022年度博客之星评选TOP 10&#x1f3c6;&#xff0c;Java领域优质创作者&#x1f3c6;&#xff0c;CSDN博客专家&#x1f3c6;&#xff0c;华为云享专家&#x1f3c6;&#xff0c;掘金年度人气作…

Java学习笔记(15)

JDK7前时间相关类 Date时间类 Simpledateformat Format 格式化 Parse 解析 默认格式 指定格式 EE&#xff1a;表示周几 Parse&#xff1a;把字符串时间转成date对象 注意&#xff1a;创建对象的格式要和字符串的格式一样 Calendar日历类 不能创建对象 Getinstance 获取当…

深度学习pytorch——索引与切片

indexing import torch a torch.rand(4,3,28,28) # 表示4张28*28的rgb图 print(a[0].shape) # a[0]获得第一张图片 print(a[0,0].shape) # a[0,0]获得第一张图片的r图 print(a[0,0,2,4]) # 获得第一张图片第一个通道的一个像素点&#xf…

【C#】【SAP2000】读取SAP2000中所有Frame对象在指定工况的温度荷载值到Grasshopper中

if (build true) {// 连接到正在运行的 SAP2000// 使用 COM 接口获取 SAP2000 的 API 对象cOAPI mySapObject (cOAPI)System.Runtime.InteropServices.Marshal.GetActiveObject("CSI.SAP2000.API.SapObject");// 获取 SAP2000 模型对象cSapModel mySapModel mySap…

4262. 空调(acwing)

文章目录 4262. 空调题目描述贪心差分难点解析差分数组的更新计算最小命令数量更新差分数组反向差分计算计算最小指令数量 4262. 空调 题目描述 Farmer John 的 N头奶牛对他们牛棚的室温非常挑剔。 有些奶牛喜欢温度低一些&#xff0c;而有些奶牛则喜欢温度高一些。 Farmer…

MySQL数据库操作学习(1)

文章目录 一_初识MySQL数据库1、数据库2、数据库分类1.关系型数据库2.非关系型数据库&#xff08;备用&#xff09; 3、数据库概念1.数据&#xff08;data&#xff09;2.数据库&#xff08;database&#xff09;3.数据管理系统&#xff08;DBMS&#xff09; 二、MySQL库操作了解…

315曝光黑灰产业链:主板机

关注卢松松&#xff0c;会经常给你分享一些我的经验和观点。 315晚会曝光主板机黑灰产业链&#xff0c;主板机是什么呢?可能很多人还不知道。在这里松松给大家普及一下&#xff0c;也欢迎大家关注卢松松哟! 主板机是什么呢? 通过报废手机的主板&#xff0c;拆出来后组装成主…

【XR806开发板试用】xr806 RTC实验

一、例程编译、烧录确认 首先按照全志在线文档平台的点灯教程确保能正常编译、烧录和点灯&#xff1a;https://xr806.docs.aw-ol.com/… 确保例程没问题后&#xff0c;我们再改造例程&#xff0c;实现我们想要的功能 二、代码编写 我们将hello工程复制一份改文件夹名为rtc_demo…

专升本 C语言笔记-06 常用的3种输入输出函数

1.scanf() 与 printf() 的使用 scanf() 格式化输入数据 格式:scanf("格式控制字符串",参数地址列表) scanf("%d,%d,%d",&a,&b,&c); printf("a %d\n",a); printf("b %d\n",b); printf("c %d\n",c); 注意 注…

【数据结构】堆

目录 一、树的介绍以及堆 1.树 2.二叉树以及堆 二、堆的实现 1.heap.h 2.heap.c 1)堆的初始化和销毁 2&#xff09; 堆的插入 3&#xff09;堆的删除 4&#xff09;取堆顶数据 5&#xff09;堆的数据个数 6&#xff09;堆的判空 3.test.c 一、树的介绍以及堆 …

跨境电商选品实战——Ownips公开数据信息安全采集+Python爬虫轻松搞定Lazada电商选品

文章目录 一、引言二、Lazada电商平台选品实战2.1、分析Lazada电商平台的商品列表接口2.2、定位商品列表计算逻辑2.3、封装高质量住宅IP2.4、运行爬虫 三、数据处理及选品分析四、Ownips——企业级全球静态住宅IP&#xff0c;高效采集公开数据 一、引言 互联网与外贸的结合&am…

Maya自定义工具架

有时候我们需要自己定义工具架上的内容&#xff0c;比如将一个工具放到工具架上&#xff0c;或者删除一个工具 添加一个工具 例如我们想在多边形建模栏位上添加一个分离按钮&#xff0c;默认 1 先切换到想要添加的工具架栏位 2 打开菜单&#xff0c;找到我们想添加的工具 …

前端实现websocket通信讲解(vue2框架)

websocket&#xff1a; WebSocket是HTML5下一种新的协议&#xff08;websocket协议本质上是一个基于tcp的协议&#xff09;它实现了浏览器与服务器全双工通信&#xff0c;能更好的节省服务器资源和带宽并达到实时通讯的目的Websocket是一个持久化的协议 websocket提供的api&a…

70城市房价同比继续下降

据北京商报的最新报道&#xff0c;昨&#xff08;3月15日&#xff09;天国家统计局发布《2月70个大中城市商品住宅销售价格》显示&#xff0c;2024年2月&#xff0c;在70个大中城市中&#xff0c;各线城市商品住宅销售价格同比继续下降。 一线城市二手住宅销售价格同比下降6.3…

拿捏指针(二)

个人主页&#xff1a;秋邱博客 所属栏目&#xff1a;C语言 &#xff08;感谢您的光临&#xff0c;您的光临蓬荜生辉&#xff09; 目录 前言 数组与指针 数组名的理解 指针数组与数组指针 指针数组 数组指针 数组传参 一维数组传参的本质 二维数组传参的本质 二维数组…

Github Gitlab SSH 密钥配置

1. 操作流程 生成密钥&#xff08;非对称加密&#xff09; ssh-keygen -t rsa -C "your_emailexample.com"注意&#xff0c;这里的 -C 表示注释&#xff0c;没有什么用&#xff0c;主要是标记密钥避免自己遗忘。 查看密钥&#xff08;公钥&#xff09; //进入所属…

Go 日期时间包装器:15条更便捷的时间处理

在Go编程中&#xff0c;处理日期和时间是一项常见任务&#xff0c;涉及到精确性和灵活性。尽管Go的标准库提供了时间包&#xff08;time&#xff09;用于处理时间相关操作&#xff0c;但在某些情况下&#xff0c;我们需要额外的实用函数来简化这些任务。本文将介绍一系列实用函…

RIPGeo参文31—36(关于对比学习):鼓励对同一数据点进行各种增强(视图),以学习更健壮的表示

RIPGeo中有: —干扰参数。在内部最大化中,我们提出了步骤,以增加损失的方向更新。我们的方法不是用简单的一步方案最大化内部部分,而是在每次迭代结束时将扰动投影到球面空间上(第2-7行),这允许模型产生更微妙但有价值的扰动[31]。 [31] A. Kurakin, I. J. Goodfellow…

代码随想录算法随想录day22 | 235. 二叉搜索树的最近公共祖先、701.二叉搜索树中的插入操作、450.删除二叉搜索树中的节点

目录 二叉搜索树的最近公共祖先思路解题方法复杂度Code 二叉搜索树中的插入操作思路解题方法复杂度Code 删除二叉搜索树中的节点思路解题方法复杂度Code 总结 二叉搜索树的最近公共祖先 链接: 二叉搜索树的最近公共祖先 给定一个二叉搜索树, 找到该树中两个指定节点的最近公共…

【智能硬件、大模型、LLM 智能音箱】MBO:基于树莓派、ChatGPT 的桌面机器人

MAKER:David Packman/译:趣无尽(转载请注明出处) 这是国外 Maker David Packman 制作的基于树莓派机器人 MBO,该机器人的外观设计灵感来自动漫 Adventure Time 中的机器人 MBO。它具有强大的交互功能,可实现脱机唤醒词检测、调用 ChatGPT 3.5 进行聊天、机器视觉对图像进…