事故的背景

简单的描述一下业务场景,项目里面有一个分布式定时job ,定期去扒取数据,那么有两层循环,第一层是大概1000条数据
,然后第二层循环每个数据下面大概20个子数据,然后通过多线程的方式去扒取数据入库。这就是一个简单的业务背景。这个对刚入行的小白写代码得注意了!

作为程序员的我也是第一次遇到这个问题,虽然这个问题解决很简单,但是造成的影响很大,我觉得还是有必要做一个小总结,小伙伴们以后写类似代码的时候就会有注意,这个问题的造成是我一个同事没有理解透线程池,导致的一个很大的问题。那么先说问题之前先扒拉扒拉线程池。

线程池

首先我先说明一点在企业中一般都是自定义线程池,很少使用jdk 给我们提供的几种线程池方法,这样是为了做到一个可控制。

这里帮大家只是一次简单的回顾吧,具体线程池网上已经一大片一大片的文章了。

  public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,RejectedExecutionHandler handler) private static ThreadPoolExecutor poolExecutor = newThreadPoolExecutor(3,30,1,TimeUnit.MINUTES,new ArrayBlockingQueue(1000),new MyRejectedExecutionHandler(MAX_QUEUE_SIZE));

那么说到线程池无非就理解这几个参数。

流程

  • 假如我们定义的corePoolSize 是5个 maximunPoolSize 是 30, keepAliveTime为一分钟 ,队列大概1000个吧
  • 第1个Task来了放入到队列里面,线程池一直从队列里面获取,获取到第一个Task,发现当前线程池里面线程个数小于corePoolSize ,欧克欧克,兄弟我马上给你创建一个线程。
  • 此时第2个Task 又来了,线程池又从队列里面获取,发现现在的线程还是小于corePoolSize,马上又开启新的线程继续执行
  • 此时第5个Task 来了,那么和corePoolSize 相等了,ok ,那么继续创建一个线程。对于创建的这5个线程是不会销毁的,这也是为什么线程池可以避免重复创建线程代来的消耗。
  • 此时第6个Task 来了,线程池发现corePoolSize 里面有刚刚执行完了的线程,那么此时就不需要创建新的线程了,直接拿来使用,达到了一个复用的效果。
  • 假如我们的Task 很多很多,又比较耗时,corePoolSize 已经没有空闲的线程了,那么接下来就是maximumPoolSize出场了,发现线程池里面此时corePoolSize < num < maximumPoolSize
    那么继续创建线程执行Task
  • 那么当线程池数量num > maximumPoolSize,就不会创建新的线程,来的新的Task直接往队列里面仍进去
  • 当队列里面都已经超过了最大数量(我们这里定义的1000) ,相当我们线程池已经爆满了没法接收新的Task了,那么不好意思兄弟,我只能拒绝你了,然后就走我们的MyRejectedExecutionHandler
  • 上面的参数基本已经提到了,还剩一个keepAliveTime 那么这个时间又什么时候用呢,这种场景下corePoolSize < num < maximumPoolSize 超过corePoolSize 空闲的线程的存活时间,比如核心是5个,最大30个,那么多余的25个线程就是超出的线程,简单的总结就是:超出核心线程的线程 存活的时间。

那么上面也是整个线程池的核心流程做了一个描述。

自定义线程池

上面已经描述了线程池的流程和原理,下面自定义线程池直接贴代码了,就不做过多的阐述了。

// 定义一个线程池类
public class MyWorkerThreadPool {private static final int MAX_QUEUE_SIZE = 1000;private static ThreadPoolExecutor poolExecutor = newThreadPoolExecutor(3,30,1,TimeUnit.MINUTES,new ArrayBlockingQueue(MAX_QUEUE_SIZE),new MyRejectedExecutionHandler(MAX_QUEUE_SIZE));public static void submitTak(Runnable run) {poolExecutor.submit(run);}public static void shutdown() {poolExecutor.shutdown();}
}// 定义一个拒绝策略public class MyRejectedExecutionHandler implements RejectedExecutionHandler{private final Log logger = LogFactory.getLog(this.getClass());private int  maxQueueSize;public MyRejectedExecutionHandler(int maxQueueSize){this.maxQueueSize=maxQueueSize;}@Overridepublic void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {System.out.println("提交任务失败了" +"当前队列最大长度" +"maxQueueSize="+maxQueueSize+",maximumPoolSize="+executor.getMaximumPoolSize());if(executor.getQueue().size()<maxQueueSize){executor.submit(r);}else{try {Thread.sleep(3000);executor.submit(r);}catch (Exception e){//此异常忽略executor.submit(r);}}}
}

事故代码(伪代码)

那么接下来就是重点了,这里只贴一部分伪代码,前面已经说了这是一个分布式定时job,这里是我精简了同事的代码提炼出来的。

 //模拟场景for(int i= 0;i< 1000;i++){for(int j = 0;j<20;j++){MyWorkerThreadPool.submitTak(()->{// 真实业务场景这里非常耗时,try {Thread.sleep(5000);} catch (InterruptedException e) {e.printStackTrace();}});}}执行结果:
提交任务失败了当前队列最大长度maxQueueSize=1000,maximumPoolSize=30
提交任务失败了当前队列最大长度maxQueueSize=1000,maximumPoolSize=30
提交任务失败了当前队列最大长度maxQueueSize=1000,maximumPoolSize=30
提交任务失败了当前队列最大长度maxQueueSize=1000,maximumPoolSize=30
提交任务失败了当前队列最大长度maxQueueSize=1000,maximumPoolSize=30

这里 第一层模拟了1000条数据,第二层循环30条数据,同事的代码,导致同一时间我们自定义的线程队列爆满,2000*30 这个是要开多少个线程啊。细细想下是不是很恐怖,这么使用会导致什么后果呢,当前业务又很多都被拒绝掉没有执行,另外当线程池爆满后,我们项目的其它功能执行异步方法也会被拒绝掉,结果可想而知。

那么如何解决了,其实很简单,我们跑这个比较耗时的任务我们可以指定10个线程去跑就是了,这样就不会影响到其它的功能业务。
我这里简单的使用了一个计数器,比如超过了10个线程则阻塞等待一会,跑完一个线程则减一,直接上代码

public class TestMain {public static void main(String[] args) throws Exception{//模拟场景,这是一个分布式定时任务,所以不会存在并发同时执行的问题AtomicInteger threadNum = new AtomicInteger(0);// 模拟当前第一层有1000数据for(int i= 0;i< 1000;i++){// 模拟每条线路有20条子数据for(int j = 0;j<20;j++){// 多线程拉去爬取网上的数据汇总到数据库// 一次最多开启10个线程取执行耗时操作,while (threadNum.get() > 10){// 可以小睡一会再看是否有资格执行Thread.sleep(500);}// 小增加1threadNum.incrementAndGet();int tempI = i;int tempJ = j;MyWorkerThreadPool.submitTak(()->{// 真实业务场景这里非常耗时,try {Thread.sleep(5000);System.out.println(Thread.currentThread().getName()+"执行完第一层数据"+ tempI +"第二层:"+tempJ);// 执行完减少一个1threadNum.decrementAndGet();} catch (InterruptedException e) {e.printStackTrace();}});}}}执行结果:
pool-2-thread-2执行完第一层数据0第二层:1
pool-2-thread-1执行完第一层数据0第二层:0
pool-2-thread-3执行完第一层数据0第二层:2
pool-2-thread-2执行完第一层数据0第二层:3
pool-2-thread-3执行完第一层数据0第二层:5
pool-2-thread-1执行完第一层数据0第二层:4
pool-2-thread-3执行完第一层数据0第二层:7
pool-2-thread-1执行完第一层数据0第二层:8
pool-2-thread-2执行完第一层数据0第二层:6
pool-2-thread-1执行完第一层数据0第二层:10
pool-2-thread-3执行完第一层数据0第二层:9
pool-2-thread-2执行完第一层数据0第二层:11
pool-2-thread-3执行完第一层数据0第二层:13
pool-2-thread-2执行完第一层数据0第二层:14
pool-2-thread-1执行完第一层数据0第二层:12

总结

其实我们开发中又很多小细节,只是我们有时候没有注意或对其原理不清楚,有时候写出来的代码就会带来比较糟糕的后果。那么今天这篇就到这里!

滥用线程,导致线上线程池被撑爆的一次意外相关推荐

  1. mysql爆内存_线上MySQL数据库机器内存爆掉原因分析与解决

    本文主要向大家介绍了线上MySQL数据库机器内存爆掉原因分析与解决,通过具体的内容向大家展现,希望对大家学习MySQL数据库有所帮助. 现象: 阿里金融某业务的MySQL机器的内存每隔几天就会增长,涨 ...

  2. 一次排查线上线程池数量过高的报警经历

    线上jstack查看正常机器和问题机器对比堆栈信息,发现大量的http-nio-1601-exec-线程在等待,查看线上监控发现优惠券接口调用量飙升,中午商家做活动抢券,根据线程名字可以看出是tomc ...

  3. 一次jvm导致线上内存占用过高问题定位

    背景:8G物理内存,8核CPU,jvm使用的G1垃圾回收器. 问题:线上内存占用告警,内存占用超过85%,且现象一直持续. 分析 看一下jvm启动参数配置: -Xms6144m -Xmx6144m - ...

  4. 基本类型为空导致线上空指针异常问题 java.lang.NullPointerException: cannot unbox null value

    线上钉钉群突然报空指针异常,结合日志分析代码,如下: 报错信息:(含入参) reQueryDto={\"minAge\":null,\"maxAge\":26, ...

  5. 万万没想到! logger.info() 还能导致线上故障?

    事故代码 直入主题,生产环境日志级别为warn,请看如下这行代码: LOGGER.info("the DTO info: {}", JSON.toJSONString(DTO)); ...

  6. GC导致线上CPU超100%

    运维同学告知报警,一个java进程占用CPU 100%以上.下面进行排查步骤. 1.top -c ,效果如下图(这个截图是另一个节点的效果,实际进程号为17817,后续的截图将都会以进程号17817为 ...

  7. oracle的clob字段导致线上应用无响应问题及解决

    项目中有一张日志表,里面有个clob字段,专门用来保存定时任务的执行日志,上线已经一年多了,一直用的好好的,前两天突然发现有个任务的日志查询不出来了,界面上一直处于卡死状态,而且系统开始报警,资源占用 ...

  8. 【好记性不如烂笔头】记一次线上问题,系统卡爆了,排查日志发现出现了ownerThread current state is WAITING, current stackTrace

    问题描述   项目部署在linux环境,运行期间页面访问特别卡,功能无法正常使用. 原因分析:   排查后台日志发现出现了"ownerThread current state is WAIT ...

  9. 在被线上大量日志输出导致性能瓶颈毒打了很多次之后总结出的经验

    由于线上业务量级比较大(日请求上亿,日活用户几十万),同时业务涉及逻辑很复杂,线上日志级别我们采用的是 info 级别,导致线上日志量非常庞大,经常遇到因为日志写入太慢导致的性能瓶颈(各微服务每小时日 ...

最新文章

  1. GO语言有哪些优势?怎样入门?
  2. R语言构建混淆矩阵(仿真数据)并基于混淆矩阵(confusion matrix)计算并计算Accuracy、Precision、Recall(sensitivity)、F1、Specificity指标
  3. ModuleNotFoundError: No module named ‘cx_Oracle‘
  4. C语言-main方法的两个参数是干什么的?
  5. Apache Kylin Cube 的构建过程
  6. 零基础也可以实现“机器同传翻译”!
  7. swiper移入暂停_react中swiper注意事项及鼠标划入停止轮播
  8. C 语言笔记: 链表节点实现技巧--struct的妙用
  9. Android Service被系统回收的解决方法
  10. 软件架构设计箴言理解
  11. spark多个kafka source采用同一个group id导致的消费堆积延迟
  12. PostgreSQL 12系统表(9)pg_settings
  13. vue跳转传参刷新后参数消失
  14. TensorFlow2.0:维度变换
  15. AD程序设计c语言,AVR AD转换的C语言编程
  16. Java-虚拟机-栈帧
  17. C++ SHFileOperation实现文件、文件夹拷贝、删除、重命名
  18. GDAL(Geospatial Data Abstraction Library )简介
  19. 厂级监控系统镜像服务器,厂级监控信息系统(SIS).PDF
  20. hdu 1789题解

热门文章

  1. linux les命令,Linux用户和组命令
  2. 用python-webdriver实现自动填表
  3. 西山居剑心数据分析笔面试题
  4. adb unlock
  5. FCKeditor源代码分析(一)-----fckeditor.js的中文注释分析(原创)
  6. yolov5在线检测目标检测网页实时识别python目标检测flask
  7. redis:redis介绍和安装、普通连接和连接池、redis 5大数据类型之字符串、Hash、列表、其他操作(通用)、管道、django使用redis、接口缓存
  8. 计算机游戏act指的是,act 和 sct 是什么意思
  9. 模拟button按钮按下
  10. 【python】choice函数