1. 线程池参数详解
先讲讲线程池的参数含义,网上相关的说明很多,如果比较了解可以略过此处
这是ThreadPoolExecutor最全的构造器:
ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
-
corePoolSize: 线程池中核心线程数量(经常干活的线程的数量),后续简称为coreSize
-
workQueue:当前线程数量等于coreSize的时候,新来的任务保存的地方,等着有空闲线程的时候再执行,后续简称为queue
-
maximumPoolSize:当前线程数量等于coreSize并且queue也满了,这时候就得额外创建线程了,创建之后线程池中的最大线程数就是由这个值决定的,后续简称为maxSize
-
keepAliveTime:空闲线程能存活的时间数值
-
unit:空闲线程能存活的时间单位
-
threadFactory:创建线程的工厂,可以指定线程的名字啊等等
-
handler:当前线程数等于maxSize,并且queue也满了,对于新来的任务的处理策略(是丢掉呢还是抛出异常呢)
先很形象的理解一下,真的很形象
场景:线程池就是一个包工头手下的工人,coreSize就是工人的数量,task就是搬砖,queue就是车
第二种情况:有部分工人在搬,有部分在休息,这会拖了一些砖过来,休息的人也开始搬砖了
第二种情况:每个工人都在辛苦的搬着砖,相当于当前线程数等于coreSize,但是砖还在源源不断的运过来,这时候车还有空位置呢,行吧,砖先放车上,待会空了就搬
第三种情况:每个工人都在辛苦的搬着砖,车也装满了,但是这会又来砖了,怎么办呢?包工头就想着,那要不我再临时雇两个人吧
第四种情况:每个工人都在辛苦的搬着砖,车也装满了,包工头还临时雇了两个人,这时候又拖了些砖过来,怎么办呢?包工头就想我都雇了两个临时工了,再雇人就不划算了,算了吧,这些砖让其他包工头搬吧
第五种情况:砖搬得差不多了,等了半天也没砖运过来,刚请的临时工就让他们结账走人吧,不然好亏哦,还是只留下原班人马
总结
当前线程小于coreSize时,创建新线程执行任务
当前线程等于coreSize且queue未满时,将任务放进queue,不创建新的线程
当前线程等于coreSize且queue已满时,创建新的线程来执行任务
当前线程等于maxSize且queue已满时,执行拒绝策略
当任务变少,线程开始空闲,空闲时间超过设置,则销毁多余线程(除了核心线程数量的其他线程)
2. 常见参数的计算方法
在指定参数之前先弄清楚这两个问题
1. 任务并发有多大?
2. 每个任务执行时间?
假设一秒钟的并发量在100-200之间,每个任务执行的时间为100ms
-
corePoolSize
最大并发量 x 每个任务的执行时间 x 80% (保证百分之八十的任务都有现成的线程来处理)
200 * 0.1 * 80% = 16
-
workQueue
每秒最大并发量 - corePoolSize x (每秒处理的任务数量)
当任务并发量超过核心线程能处理的任务数极限了,就将多余的放入queue200 - 16 * (1 / 0.1)= 40
-
maximumPoolSize
最大并发量 x 每个任务的执行时间
200 * 0.1 = 20
其他的参数都得根据具体的业务场景来指定了
3. 实例讲解验证
场景:在Spring aop中获取请求日志,异步保存至数据库,验证线程池参数是否满足要求
下面是核心代码:
@Slf4j
@Aspect
@Component
public class UserLogAspect {
/**
* 我这里将拒绝策略指定为:new Reject() 使用备用线程继续执行任务,如果使用到了备用线程则
* 会打印出:【当前并发较高,超过200/s,进入备用线程保存日志数据,请优化线程池参数】
*/
private static ThreadPoolExecutor executor = new ThreadPoolExecutor(16,
20,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(40), Thread::new, new Reject());
@Resource
private LogService logService;
@Pointcut(value = "@annotation(com.hrong.major.annotation.ClickLog)")
public void pointcut() {
}
@Around("pointcut()")
public String aroundController(ProceedingJoinPoint joinPoint) throws Throwable {
try {
HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
//获取请求数据,省略相关代码
Object response = joinPoint.proceed();
Log logInfo = new Log(日志相关参数);
executor.execute(() -> logService.save(logInfo));
return response.toString();
} catch (Exception e) {
e.printStackTrace();
log.error("出现异常:{}", e.getMessage());
Object response = joinPoint.proceed();
return response.toString();
}
}
static class Reject implements RejectedExecutionHandler {
private ThreadPoolExecutor rejectExecutor = new ThreadPoolExecutor(8,
10,
60L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(20), (ThreadFactory) Thread::new);
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
log.warn("【当前并发较高,超过200/s,进入备用线程保存日志数据,请优化线程池参数】");
rejectExecutor.execute(r);
}
}
}
先清空log表
再启动Apache ab(一个简单的压力测试工具)开始测试
ab -n 100 -c 50 "http://localhost:8081/majors/5?page=1&size=20"
100表示请求数量
50表示并发数量
1. 并发50
执行结果部分截图表示请求时间最长的是568ms
然后再看看控制台日志,也没有使用备用线程池的日志
再看看数据库,数据都成功保存了
2. 并发100
控制台
数据库
3. 并发200
控制台提示使用了备用线程池,提示我们该优化参数了
数据库,没有丢数据