Java避坑案例 - 线程池错误的混用引发的性能故障分析
文章目录
- 问题现象
- 问题分析
- 问题修复
- 线程池的混用策略
- 任务类型与线程池配置
- 最佳实践
问题现象
代码使用了线程池异步处理一些内存中的数据,但通过监控发现处理得非常慢,整个处理过程都是内存中的计算不涉及 IO 操作,也需要数秒的处理时间,应用程序 CPU 占用也不是特别高,有点不可思议
问题分析
经排查发现,业务代码使用的线程池,除了自身业务使用,同时还被一个后台的文件批处理任务用到了。
private static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 2,1, TimeUnit.HOURS,new ArrayBlockingQueue<>(100),new ThreadFactoryBuilder().setNameFormat("batchfileprocess-threadpool-%d").build(),new ThreadPoolExecutor.CallerRunsPolicy());public int handleFile() throws ExecutionException, InterruptedException {return threadPool.submit(calcTask()).get();}
接下来我们模拟一下文件批处理的代码,在程序启动后通过一个线程开启死循环逻辑,不断向线程池提交任务,任务的逻辑是向一个文件中写入大量的数据
/*** 在Bean初始化完成后执行的方法* 此方法初始化线程池并启动一个新线程来执行特定任务*/
@PostConstruct
public void init() {// 打印线程池的初始状态和统计信息printStats(threadPool);// 创建并启动一个新的线程来执行批处理任务new Thread(() -> {// 生成一个由'a'字符组成的100万字符长的字符串作为负载String payload = IntStream.rangeClosed(1, 1_000_000).mapToObj(__ -> "a").collect(Collectors.joining(""));// 无限循环执行任务while (true) {// 使用线程池执行写入操作threadPool.execute(() -> {try {// 写入当前时间戳和负载到文件中Files.write(Paths.get("artisan.txt"), Collections.singletonList(LocalTime.now().toString() + ":" + payload), UTF_8, CREATE, TRUNCATE_EXISTING);} catch (IOException e) {// 打印异常信息e.printStackTrace();}// 记录日志,表示批处理任务完成log.info("batch file processing done");});}}).start();
}/*** 定期打印线程池的运行统计信息* 此方法内部创建了一个新的单线程调度器,用于定期执行打印线程池统计信息的任务* 它提供了线程池大小、活动线程数、已完成任务数和队列中任务数的信息* 这些信息有助于监控线程池的性能和工作负载* * @param threadPool 线程池对象,其统计信息将被打印*/
private void printStats(ThreadPoolExecutor threadPool) {Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> {// 打印分割线,用于区分不同的统计时间点log.info("=========================");// 打印线程池当前的线程数量log.info("Pool Size: {}", threadPool.getPoolSize());// 打印当前活动线程的数量log.info("Active Threads: {}", threadPool.getActiveCount());// 打印已完成任务的总数log.info("Number of Tasks Completed: {}", threadPool.getCompletedTaskCount());// 打印队列中等待执行的任务数量log.info("Number of Tasks in Queue: {}", threadPool.getQueue().size());// 再次打印分割线,结束本次统计信息的打印log.info("=========================");}, 0, 1, TimeUnit.SECONDS);
}
流程如下:
通过 printStats 方法打印出的日志可以看到,这个线程池中的 2 个线程任务是相当重的。

-
线程池的 2 个线程始终处于活跃状态,队列也基本处于打满状态
-
因为开启了CallerRunsPolicy 拒绝处理策略,所以当线程满载队列也满的情况下,任务会在提交任务的线程,或者说调用 execute 方法的线程执行,也就是说不能认为提交到线程池的任务就一定是异步处理的。如果使用了 CallerRunsPolicy 策略,那么有可能异步任务变为同步执行。从日志的第四行也可以看到这点。这也是这个拒绝策略比较特别的原因
试想一下: 业务代码复用这样的线程池来做内存计算,会怎样???
写一段代码测试下,向线程池提交一个简单的任务,这个任务只是休眠 10 毫秒没有其他逻辑
/*** 创建一个计算任务,该任务在执行时会暂停一段时间然后返回固定的结果* * @return Callable<Integer> 一个计算任务,当运行时会暂停10毫秒,然后返回整数1*/
private Callable<Integer> calcTask() {return () -> {// 暂停10毫秒,模拟耗时操作或等待资源TimeUnit.MILLISECONDS.sleep(10);// 返回固定的结果1,表示任务完成return 1;};
}/*** 错误的异步任务处理示例* * 此方法展示了一个错误的异步任务处理方式,通过提交一个计算任务到线程池并立即获取结果的方式* 这种做法未能正确处理异步任务,因为它会导致线程池的线程阻塞,直到任务完成,从而降低了系统的整体性能和响应速度* * @return 计算任务的结果* @throws ExecutionException 如果计算任务执行失败* @throws InterruptedException 如果线程被中断*/
@GetMapping("wrong")
public int wrong() throws ExecutionException, InterruptedException {return threadPool.submit(calcTask()).get();
}
使用 wrk 工具对这个接口进行一个简单的压测,可以看到 TPS 为 75,性能的确非常差

细想一下,问题其实没有这么简单。因为 原来执行 IO 任务的线程池使用的是CallerRunsPolicy 策略,所以直接使用这个线程池进行异步计算的话,当线程池饱和的时候,计算任务会在执行 Web 请求的 Tomcat 线程执行,这时就会进一步影响到其他同步处理的线程,甚至造成整个应用程序崩溃。
问题修复
解决方案很简单,使用独立的线程池来做这样的“计算任务”即可。计算任务打了双引号,是因为我们的模拟代码执行的是休眠操作,并不属于 CPU 绑定的操作,更类似 IO 绑定的操作,如果线程池线程数设置太小会限制吞吐能力
/*** 创建一个固定大小的线程池用于异步计算任务* * 线程池的配置参数说明:* - 核心线程数和最大线程数都设置为200,意味着线程池只会创建200个线程来执行任务* - 线程空闲时间设为1小时,即线程在空闲1小时后将被终止* - 使用ArrayBlockingQueue作为任务队列,队列大小设为1000,这意味着在队列满的情况下,* 新提交的任务将等待直到队列中有空位* - 通过ThreadFactoryBuilder设置线程名称格式,以便于追踪和管理线程*/
private static ThreadPoolExecutor asyncCalcThreadPool = new ThreadPoolExecutor(200, 200,1, TimeUnit.HOURS,new ArrayBlockingQueue<>(1000),new ThreadFactoryBuilder().setNameFormat("asynccalc-threadpool-%d").build());/*** 使用异步方式计算并返回结果* 本方法通过提交一个计算任务到异步计算线程池,并等待任务完成后的结果* 如果任务抛出异常,将会被传播到调用者* * @return 计算结果* @throws ExecutionException 如果计算任务执行失败或被取消* @throws InterruptedException 如果等待结果时线程被中断*/
@GetMapping("right")
public int right() throws ExecutionException, InterruptedException {// 提交一个异步计算任务到线程池并获取Future对象,用于获取计算结果return asyncCalcThreadPool.submit(calcTask()).get();
}
使用单独的线程池改造代码后再来测试一下性能,TPS 提高到了 1727

在这个案例中,使用了一个只包含两个核心线程的线程池,同时被 I/O 密集型和 CPU 密集型任务共享,导致了性能瓶颈。由于采用了 CallerRunsPolicy 拒绝策略,当线程池达到饱和时,任务被回退到调用线程执行,进而影响了整个应用的性能。
这种配置的影响是显而易见的:异步任务的执行可能变成了同步执行,进一步降低了应用的响应能力。
线程池的混用策略
线程池的设计确实是为了复用资源,但这并不意味着所有任务都应共享同一个线程池。不同类型的任务(如 I/O 密集型与 CPU 密集型)对线程池的需求差异显著,因此需要谨慎选择线程池的配置和混用策略。
任务类型与线程池配置
- I/O 密集型任务:如网络请求或文件操作,通常需要更多的线程来处理潜在的阻塞,因此可配置较高的核心线程数,且队列大小适中。
- CPU 密集型任务:计算密集型任务应该限制线程数量,通常设置为 CPU 核数或 CPU 核数的两倍,以减少线程切换的开销。这类任务可能需要更大的队列来缓冲任务。
最佳实践
- 任务隔离:为不同类型的任务创建独立的线程池,避免混用。
- 合理配置:根据任务的特性(I/O 密集或 CPU 密集)合理设置线程池的核心参数。
- 监控与调优:定期监控线程池的性能指标,根据实际需求进行调整。

相关文章:
Java避坑案例 - 线程池错误的混用引发的性能故障分析
文章目录 问题现象问题分析问题修复线程池的混用策略任务类型与线程池配置最佳实践 问题现象 代码使用了线程池异步处理一些内存中的数据,但通过监控发现处理得非常慢,整个处理过程都是内存中的计算不涉及 IO 操作,也需要数秒的处理时间&…...
七种方法助你找到实用且免费的API服务
随着现代互联网的迅猛发展,API(应用程序编程接口)已成为推动技术创新的核心工具。API使得开发者能够快速实现复杂的功能,如数据分析、自然语言处理、图像识别等,而无需从头编写大量的代码。在这个开放的生态中…...
leetcode-74-搜索二维矩阵
题解: 遍历二维数组matrix中的每个list,如果target在list中则返回True(退出循环);如果全部遍历完还没有出现target则返回False。 核心就是在list中查找target! 代码实现:...
122.WEB渗透测试-信息收集-ARL(13)
免责声明:内容仅供学习参考,请合法利用知识,禁止进行违法犯罪活动! 内容参考于: 易锦网校会员专享课 上一个内容:121.WEB渗透测试-信息收集-ARL(12) 输入命令: docker…...
动态规划 —— 路径问题-下降路径最小和
1. 下降路径最小和 题目链接: 931. 下降路径最小和 - 力扣(LeetCode)https://leetcode.cn/problems/minimum-falling-path-sum/description/ 2. 算法原理 状态表示:以莫一个位置位置为结尾 dp[i,j]表示:到…...
【Linux网络】TCP_Socket
目录 TCP协议(传输控制协议) listen状态 accept和connect TCP_echo_server (1)创建套接字 (2)绑定 (3)设置listen状态 (4)loop (5)客户端 多线程远程…...
NVR批量管理软件/平台EasyNVR多个NVR同时管理支持视频投放在电视墙上
在当今智能化、数字化的时代,视频监控已经成为各行各业不可或缺的一部分,无论是公共安全、交通管理、企业监控还是智慧城市建设,都离不开高效、稳定的视频监控系统的支持。而在这些应用场景中,将监控视频实时投放到大屏幕电视墙上…...
Springboot集成阿里云通义千问(灵积模型)
我这里集成后,做成了一个工具jar包,如果有不同方式的,欢迎大家讨论,共同进步。 集成限制: 1、灵积模型有QPM(QPS)限制,每个模型不一样,需要根据每个模型适配 集成开发思路: 因有…...
微信公众号(或微信浏览器)获取openId(网页授权)
下单支付需要openId 首先授权去拿到code --然后调用后太换取openId 1.去拿取code 下图中执行到window.location.href ( redirect_uri 传入当前路径-)–执行后重新跳转到当前页面–但是路径上会带上code参数 //然后调用后台方法–将code传给后台得到 o…...
C++算法第五天
本篇文章继续和大家一起刷算法题 第一题 题目链接 . - 力扣(LeetCode) 题目解析 题目要求: 这是一个连续的子数组 计算子数组内元素的和,若数组内元素的和符合 > target的值并且该子数组的长度是最短的,则返回…...
牛客网剑指Offer-树篇-JZ26 树的子结构
题目 来源:JZ26 树的子结构 描述 输入两棵二叉树A,B,判断B是不是A的子结构。(我们约定空树不是任意一个树的子结构) 假如给定A为{8,8,7,9,2,#,#,#,#,4,7},B为{8,9,2},2个树的结构如下ÿ…...
FFmpeg 4.3 音视频-多路H265监控录放C++开发六,使用SDLVSQT显示yuv文件
使用QT 显示YUV 文件 在最后一帧的时候会不停的显示最后一帧图片。 Vsqtshowyuv.h #pragma once#include <QtWidgets/QWidget> #include "ui_vsqtshowyuv.h" #include <sdl/SDL.h> #include <iostream> #include <fstream> #include <Q…...
Spring 设计模式之适配器模式
Spring 设计模式之适配器模式 适配器模式用到的场景java举例 适配器模式 适配器模式(Adapter Pattern)是一种结构型设计模式,它允许接口不兼容的类一起工作。 其核心思想是通过一个适配器类将不兼容的接口转换成客户端期望的另一个接口&…...
多传感器数字化分析系统
在工业飞速发展的今天,设备的安全稳定运行成为企业高效生产的关键因素。然而,传统的人工巡检方式面临着诸多挑战,如效率低下、漏检误检以及难以精准掌握设备运行状态等。旗晟凭借深厚的技术积累和创新精神,推出了多传感器数字化分…...
Java 基础教学:面向对象编程基础-封装、继承与多态
面向对象编程(OOP)是现代编程的重要范式,Java 语言提供了丰富的 OOP 特性,主要包括封装、继承和多态。本文将详细讲解这三个概念及其实现方式,并提供相应的代码示例。 1. 封装 1.1 概念 封装是将对象的状态…...
Ubuntu环境本地部署DbGate数据库管理工具并实现无公网IP远程访问
文章目录 前言1. 安装Docker2. 使用Docker拉取DbGate镜像3. 创建并启动DbGate容器4. 本地连接测试5. 公网远程访问本地DbGate容器5.1 内网穿透工具安装5.2 创建远程连接公网地址5.3 使用固定公网地址远程访问 前言 本文主要介绍如何在Linux Ubuntu系统中使用Docker部署DbGate数…...
【AI抠图整合包及教程】Meta SAM 2:视觉分割的革命性飞跃
在人工智能的浪潮中,每一次技术的革新都如同一场视觉盛宴,让我们见证着数字时代的变迁。Meta再次以Segment Anything Model 2(SAM 2)引领了图像和视频分割技术的新纪元。作为首个用于实时、可提示的图像和视频对象分割的统一模型&…...
使用语言模型进行文本摘要的五个级别(llm)
视频链接:5 Levels Of LLM Summarizing: Novice to Expert...
ubuntu交叉编译libffi库给arm平台使用
1.下载并解压: 2.生成makefile 编译: make 编译成功: 安装: make install 安装成功 查看安装后的libffi库...
【jvm】空间分配担保策略
目录 1. 说明2. 工作原理2.1 估算新生代存活对象大小2.2 判断老年代的剩余空间2.3 触发Full GC的条件 3. 相关参数与配置3.1 -XX:HandlePromotionFailure3.2 -XX:PretenureSizeThreshold3.3 -XX:MaxTenuringThreshold3.4 -XX:TargetSurvivorRatio 4.作用与意义 1. 说明 1.在Ja…...
使用Matplotlib创建炫酷的3D散点图:数据可视化的新维度
文章目录 基础实现代码代码解析进阶技巧1. 自定义点的大小和颜色2. 添加图例和样式美化3. 真实数据应用示例实用技巧与注意事项完整示例(带样式)应用场景在数据科学和可视化领域,三维图形能为我们提供更丰富的数据洞察。本文将手把手教你如何使用Python的Matplotlib库创建引…...
HashMap中的put方法执行流程(流程图)
1 put操作整体流程 HashMap 的 put 操作是其最核心的功能之一。在 JDK 1.8 及以后版本中,其主要逻辑封装在 putVal 这个内部方法中。整个过程大致如下: 初始判断与哈希计算: 首先,putVal 方法会检查当前的 table(也就…...
【生成模型】视频生成论文调研
工作清单 上游应用方向:控制、速度、时长、高动态、多主体驱动 类型工作基础模型WAN / WAN-VACE / HunyuanVideo控制条件轨迹控制ATI~镜头控制ReCamMaster~多主体驱动Phantom~音频驱动Let Them Talk: Audio-Driven Multi-Person Conversational Video Generation速…...
【JVM面试篇】高频八股汇总——类加载和类加载器
目录 1. 讲一下类加载过程? 2. Java创建对象的过程? 3. 对象的生命周期? 4. 类加载器有哪些? 5. 双亲委派模型的作用(好处)? 6. 讲一下类的加载和双亲委派原则? 7. 双亲委派模…...
2025年渗透测试面试题总结-腾讯[实习]科恩实验室-安全工程师(题目+回答)
安全领域各种资源,学习文档,以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具,欢迎关注。 目录 腾讯[实习]科恩实验室-安全工程师 一、网络与协议 1. TCP三次握手 2. SYN扫描原理 3. HTTPS证书机制 二…...
Python Einops库:深度学习中的张量操作革命
Einops(爱因斯坦操作库)就像给张量操作戴上了一副"语义眼镜"——让你用人类能理解的方式告诉计算机如何操作多维数组。这个基于爱因斯坦求和约定的库,用类似自然语言的表达式替代了晦涩的API调用,彻底改变了深度学习工程…...
区块链技术概述
区块链技术是一种去中心化、分布式账本技术,通过密码学、共识机制和智能合约等核心组件,实现数据不可篡改、透明可追溯的系统。 一、核心技术 1. 去中心化 特点:数据存储在网络中的多个节点(计算机),而非…...
Xcode 16 集成 cocoapods 报错
基于 Xcode 16 新建工程项目,集成 cocoapods 执行 pod init 报错 ### Error RuntimeError - PBXGroup attempted to initialize an object with unknown ISA PBXFileSystemSynchronizedRootGroup from attributes: {"isa">"PBXFileSystemSynchro…...
结构化文件管理实战:实现目录自动创建与归类
手动操作容易因疲劳或疏忽导致命名错误、路径混乱等问题,进而引发后续程序异常。使用工具进行标准化操作,能有效降低出错概率。 需要快速整理大量文件的技术用户而言,这款工具提供了一种轻便高效的解决方案。程序体积仅有 156KB,…...
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ is not explicitly defined.
这个警告表明您在使用Vue的esm-bundler构建版本时,未明确定义编译时特性标志。以下是详细解释和解决方案: 问题原因: 该标志是Vue 3.4引入的编译时特性标志,用于控制生产环境下SSR水合不匹配错误的详细报告1使用esm-bundler…...
