一.使用场景
当前主流程中的业务处理越来越复杂,需要进行归纳总结是否可以异步去做来降低关键接口的耗时和风险,目前我们在分流调度降级,接单,拒单主流程中将一些镜像信息异步保留下来,当然我们可以将接单过程的打标等一系列不影响主流程的业务处理统统异步去做。
二.使用风险
使用过程中,我们初步会遇到线程池的参数设置(会影响到资源浪费,任务堆积时是否会引起oom(生产者和消费者的问题),如何根据生产设置最优参数),异常监控和处理(如何监控运行异常和善后 业务是否能接受抛弃还是重试 莫名其妙丢失任务等),模块化设计(统一入口,统一监控)等问题
三.使用过程问题总结
1.最初的参数设置
我们都知道线程池的参数设置有几个重要的参数,分别是核心线程数 最大线程数 任务队列 线程工厂 线程空闲时间 任务拒绝策略
First:
我们先来看看几个参数的意义:
(1) 核心线程数:默认情况下核心线程会一直存活,即使处于闲置状态也不会受存keepAliveTime
限制即没有任务需要执行。除非将allowCoreThreadTimeOut
设置为true,
默认是false,活动线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理。
(2)最大线程数:
当线程数>=corePoolSize 小于最大线程数时,且任务队列已满时,线程池会创建新线程来处理任务
当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常
(3)任务队列:
最大线程数的大小设置和任务队列的选择以及大小限制有关,常用队列 比如SynchronousQueue
,LinkedBlockingQueue
,
我们来看看线程池的执行规则的比较
假设任务队列没有大小限制
1.如果线程数量<=核心线程数量,那么直接启动一个核心线程来执行任务,不会放入队列中。
2.如果线程数量>核心线程数,但<=最大线程数,并且任务队列是LinkedBlockingQueue的时候,超过核心线程数量的任务会放在任务队列中排队。所以没有大小限制的话会一直添加到任务队列中,所以不会存在启动新线程。也就是说最大线程数的大小设置无意义,但是这种模式 如果生产速度过快 可能会消耗内存太多从而引oom。
3.如果线程数量>核心线程数,但<=最大线程数,并且任务队列是SynchronousQueue的时候,线程池会创建新线程执行任务,这些任务也不会被放在任务列中。这些线程属于非核心线程,在任务完成后,闲置时间达到了超时时间就会被清除。
4.如果线程数量>核心线程数,并且>最大线程数,当任务队列是SynchronousQueue的时候,会因为线程池拒绝添加任务而抛出异常。
假设任务队列存在大小限制
1.当LinkedBlockingQueue塞满时,新增的任务会直接创建新线程来执行,当创建的线程数量超过最大线程数量时会抛异常。
2.SynchronousQueue没有数量限制。因为他根本不保持这些任务,而是直接交给线程池去执行。当任务数量超过最大线程数时会直接抛异常。 SynchronousQueue 大小限制无意义。
(4)线程工厂:线程工厂,提供创建新线程的功能。一般默认。
(5)线程空闲时间设置: keepAliveTime,默认情况下核心线程会一直存活,即使处于闲置状态也不会受存keepAliveTime
限制即没有任务需要执行。
除非将allowCoreThreadTimeOut
设置为true,
默认是false。但是为true的时候 线程会退出直到为0,false时 线程会退出到活动线程等于核心线程数。
(6)任务拒绝策略:
首先我们看下什么情况 新的任务会被拒绝
1.当线程池线程数量达到最大线程数并且队列已满的时候
2.当调用shutdown和shutdownnow方法的时候,新进的任务都会被拒绝,前者会执行完剩余任务,后者会试图停止剩余任务 并返回任务列表。
那么这种情况发生的时候,新的任务会被怎样处理呢?线程池提供了哪些策略呢?
1.AbortPolicy 默认 会抛出异常 丢弃任务
2.CallerRunsPolicy 没有shutdown的话 主线程继续执行任务
3.DiscardPolicy 什么都不做 丢弃任务
4.DiscardOldestPolicy 判读shutdown 并移除队列的最后一个任务 同时线程池执行新任务而不是主线程执行新任务。
这几种策略都是内部类 实现了同一个接口 RejectedExecutionHandler 如果你愿意的话 也可以自己实现 注意判断是否shutdown的状态就好了。
源码中 提供了两个参数 一个主线程 一个当前线程池 进行处理
|
Second:
了解了这些参数的含义后 我们如何根据业务定义这些值呢
- tasks :每秒的任务数,按照午高峰假设为500~1000
- taskcost:每个任务花费时间,假设为0.01s
- responsetime:系统允许容忍的最大响应时间,假设为1s
- corePoolSize = 每秒需要多少个线程处理?
* threadcount = tasks/(1/taskcost) =tasks*taskcout = (500~1000)*0.01 = 5~10 个线程。corePoolSize差不多这个范围 假设初步设为8
- queueCapacity = (coreSizePool/taskcost)*responsetime
* 计算可得 queueCapacity = 8/0.01*1 = 800。意思是队列里的任务可以等待1s,超过了的需要新开线程来执行,不然来不及处理 超过系统容忍的最大响应时间
* 切记不能设置为Integer.MAX_VALUE,这样队列会很大,线程数只会保持在corePoolSize大小,当任务陡增时,不能新开线程来执行,响应时间会随之陡增。
- maxPoolSize = (max(tasks)- queueCapacity)/(1/taskcost)+corePoolSize
* 计算可得 maxPoolSize = (1000-800)/100 + 8= 10
* (最大任务数-队列容量)/每个线程每秒处理能力 + 核心线程数= 最大线程数
- rejectedExecutionHandler:根据具体情况来决定,任务不重要可丢弃,任务重要则要利用一些缓冲机制来处理
- keepAliveTime和allowCoreThreadTimeout采用默认通常能满足
总结下来 初步上线的核心线程数8 最大线程数10 队列容量800
2.监控后的参数设置
那么实际线上的情况 可能不是最优 所以我们需要监控相关指标 在午高峰 晚高峰的时候看看线程池中活跃线程到底有多少 超不超过核心线程数
超过之后 任务队列的大小又是多少?所以我们统一一个入口 关心系统中不同线程池的执行情况 如下
|
我们封装了自己的线程池 并实现了执行完任务的方法 在方法内 我们能获取到相关线程池的指标 如 活跃线程数 任务队列大小等 结合线程池的执行策略 进行调优参数
比如 活跃线程数始终不超过5
队列长度基本没有
就是说消费速度很快 核心线程数可以适当调小 基本够用
地址:
3.接下来我们对线程池的执行结果进行处理 我们知道提交任务后 我们可以观察执行任务的结果 如下
|
综上 我们对于线程池的最初参数设置 参数调优 统一监控指标 线程池异常处理等有了初步的了解。
遗留问题:1.如何在代码层面监测到当前是否引起了oom或者线程池异常过多 然后立即关闭线程池 减小系统的风险性?
2.如何防止丢失任务?