全国网站制作公司排名,创建网站英文,苏州的网络企业,如何修改wordpress站名前言 大家平常使用SpringBoot进行Web项目开发#xff0c;线程池会被配置成为全局可复用的工具#xff0c;生命周期随服务启动开始#xff0c;到服务停止即结束。这种“线程池托管”模式#xff0c;让多少兄弟产生美丽的错觉 #xff1a;“原来线程池会自己管理自己啊&…前言大家平常使用SpringBoot进行Web项目开发线程池会被配置成为全局可复用的工具生命周期随服务启动开始到服务停止即结束。这种“线程池托管”模式让多少兄弟产生美丽的错觉 “原来线程池会自己管理自己啊”但是当需求经理对你露出神秘的微笑“这个批量导出需求今晚就要...”。你不得不撸起袖子写下了罪恶的代码创建临时线程池这时兄弟们如果对线程池使用不当很容易给服务埋下隐患 。1 问题初现1.1 示例代码1.2 问题描述面试官不考虑任务内部的复杂度这个线程池的使用会有问题吗菜鸟方法执行完弹栈后局部变量都会被GC回收谁写的代码稳得很啊老鸟一口咖啡喷屏幕上老弟你线程池用完不用shutdown呀再问反正任务执行完内存都会被GC回收非得Shutdown一下不多余吗菜鸟......老鸟邪魅一笑倒也不是必须Shutdown但是不建议犯险尝试请看VCR演示代码演示2 走进科学实验现场验证)说明本文讨论的线程池对象及相关源码都围绕常用的java.util.concurrent.ThreadPoolExecutor类展开下文不再额外说明2.2 装备说明代码⚠️警 告紧接着将出现大量源码解析可能引起轻微不适。说明以下代码都是在一个类中queue、phantomRef这两个对象是作为全局对象专门捕捉那个肉身已死但阴魂不散的线程池对象。2.2.1 幽灵探测仪判断线程池对象是否被回收)因为验证的代码是在一个成熟的SpringBoot项目中跑的线程池对象太多了从内存分析工具上监测这单个线程池对象是否被GC回收不够直观这里借助虚引用来判断线程池对象是否被回收。2.2.2 实验1core 0max 100 任务完成后不进行shutdown2.2.3 实验2core 0max 100 任务完成后进行shutdown2.2.4 实验3core 100max 100 任务完成后不进行shutdown2.2.5 实验4core 100max 100 任务完成后进行shutdown2.2.6 任务执行代码示例解释说明任务数量是110主要是为了保证有足够多的任务让线程池所有线程能够打满任务里面要sleep 500ms也是同样的道理2.3 实验执行结果2.3.1 实验执行步骤项目启动后依次执行上述4个实验。需要注意的是时间间隔要保证前一个实验的任务全部执行完最好再留一些空挡这样监控更清晰每个实验执行完后分别执行一次2.2.1 判断线程池对象是否回收2.3.2 结果分析JVM初始状态1.实验1执行监控core 0max 100 任务完成后不进行shutdown2.实验2执行监控core 0max 100 任务完成后进行shutdown3.实验3执行监控core 100max 100 任务完成后不进行shutdown4.实验4执行监控core 100max 100 任务完成后进行shutdown3 原因解剖室接下来都是对实验3的异常现象进行的分析3.1 shutdown的五步拆解法首先看下ThreadPoolExecutor#shutdown方法截图中可以看到shutdown方法里面主要做了5个动作根据方法名称可以看出是一个检查动作这里不用细看把线程池状态置为SHUTDOWN状态重要根据方法名称可以看出是将线程池中的空闲线程进行中断重要根据注释来看是给特定场景对象使用这里不用细看尝试终止线程池中断线程、关闭线程池每一步的细节处理这里就不带大家看了有兴趣可以点开源码一步步研究下。总结下来就是线程池执行shutdown方法后线程池对象置为SHUTDOWN状态——挂上“暂停营业”牌子将线程池中空闲线程置为中断状态最终从线程池中剔除(会被GC回收)——给闲逛的线程发《解聘通知书》线程池中的线程对象会置为中断状态最终terminated3.2 为什么线程池不进行shutdown在方法弹栈后不会立即被GC回收首先我们梳理一下当前执行实验的线程方法栈、线程池、线程池中的线程之间的引用关系图如下实验3中当主方法执行完弹栈后短时间内线程池中的线程对象仍处于空闲/活跃状态。但是线程池对象已经不被主线程对象中的方法栈持有也就是图中关系1断开按照JVM的垃圾回收机制这时ThreadPoolExecutor对象已经不被GCRoot引用是要被GC回收的但是从2.3.2中的实验3执行监控来看ThreadPoolExecutor对象并没有被GC回收。疑问难道还有什么对象持有这个线程池对象的引用首先由于JVM的运行机制每一个java线程都关联一个OS线程线程对象在terminated之前(线程任务执行完之前)都不会被GC回收。上面的引用关系实际上应该是下面这样的线程池中的每个线程对象都有自己的执行方法栈对象根据2.3.2中的几个执行结果监控就能看出线程池对象的回收与线程池中的工作线程是否被全部回收是有关系的所以先预测线程池中的线程对象是持有线程池对象的引用的然后基于这个预测去源码中找理论支撑。预测存在黄色箭头依赖关系如下图3.3 线程对象为何会持有线程池对象的引用其实上面的引用关系图中所有的正向依赖关系我们不难理解。需要验证的是反向的依赖关系r 2.x和r 3.x这些反向的依赖关系都是什么时候建立的可以从下面几个问题入手去排查线程池(ThreadPoolExecutor)类结构中是否有相关对象的属性字段线程池对象中Worker集合中Worker对象创建时机Worker对象创建时是如何建立相应的依赖关系的从线程池提交任务开始从源码中可以看到worker类结构中本身定义有Thread变量属性在Worker对象创建时就为Thread属性显式赋值Worker类定义如下Worker对象创建时机如下从CompletableFuture工具任务执行方法中一步步进入源码可以看到如下关系上面两段源码截图中验证了依赖关系图中2.x3.x与r 3.x的依赖关系还剩下r 2.x依赖关系没有得到验证。首先从Worker类结构上没有找到Worker类中有定义对ThreadPoolExecutor类的显式引用并且从2.3.2的实验执行结果图中可以看到即使多次触发GC依旧没有将ThreadPoolExecutor对象回收掉所以Worker-ThreadPoolExecutor肯定是一种强引用(4种引用关系强引用、软引用、弱引用、虚引用)。那么哪些行为会让对象之间建立强引用关系呢我们先问下AI助手让它罗列出会建立引用关系的代码行为。总结归纳如下对象中的属性字段显式赋值引用数组、集合对象中添加其他对象的引用子类通过面向对象的继承多态特性引用父类中的属性字段还有一种平常关注较少的相对隐式的引用关系——内部类对象引用外部类对象从Worker的类结构来看是没有显示的对ThreadPoolExecutor的属性引用的也没有相关的数据、集合所以1,2不成立。Worker对象与ThreadPoolExecutor也没有直接或者间接的继承/实现关系所以3也不成立。最后再看Worker类定义确实是在ThreadPoolExecutor类的内部(内部类对象持有外部类对象的验证很简单不在这里赘述)。这样Worker-ThreadPoolExecutor的引用关系就能说得通了。至此引用依赖关系图中预测的r 2.x和r 3.x关系都成立了。3.4 原因逻辑梳理再次把依赖关系图贴出来一起再梳理一下当前方法main-Thread执行完main-Thread的方法栈都弹栈依赖关系1断开线程池不再被活跃线程引用不会再有任务进来当线程池任务都完成此时线程池中的线程都处于休眠(wait)状态等待任务队列中任务进来由于线程池中的线程都处于存活状态不会被回收存在线程池线程_n- worker_n - 线程池的逆向引用关系所以导致实验3中的现象——线程池和线程池中线程无法被GC回收4 线程池未shutdown的影响这里再把实验3会导致系统的出现的问题总结一下。通过2.3.2中的实验3监控图以及源码我们可以看到线程池使用完未及时进行shutdown就最差的情况来说会导致的问题线程池对象无法被GC回收——内存泄露2. 线程对象无法被GC回收——内存泄漏3. 从线程获取任务的源码来看即使任务队列中是空的只要线程池状态仍处于Running线程会定时从wait状态苏醒重新获取任务——占用CPU执行分片血泪教训总结线程池用完一定要养成shutdown的习惯因为有的没关好真的会漏水(内存泄露)线程池创建线程命名一定要有业务相关标识建议采用“业务场景线程计数”法订单导出-xx、邮件发送-xx出问题时秒锁嫌疑人就是订单导出线程在摸鱼内部类使用要谨慎谈恋爱可以玩失踪不行别让人家GC找不到你文末彩蛋以后你在代码里看到随手创建的线程池最终没有.shutdown()请像看到有人上厕所不冲水一样大喊“同学你线程池忘关了”