参数的构成
核心线程数、最大线程数、等待队列、拒绝策略、时间单位、时间数量(当线程数大于核心线程数时,多余的空闲线程存活的最长时间)、线程工厂类。
为什么不推荐使用四种默认的线程池?
四种默认的线程池分别是:
Cached Thread Pool:创建一个可缓存的线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,但是如果没有可以回收的线程,则新建线程(但是线程最大并发数不可控制)。
Fixed Thread Pool:固定大小的线程池,可以控制最大并发线程数,超出线程之后会在队列中等待(但是使用的是无界的LinkedBlockingQueue,任务队列最大长度为Integer.Max_value,可能会堆积大量的请求,从而造成OOM)。
ScheduleThreadPool:定时线程池,只是定时和周期性的任务执行。(但是使用的是无界的延迟阻塞队列delayedWorkQueue,任务队列最大长度为Integer.Max_value,可能会堆积大量的请求,从而造成OOM)。
SingleThreadExecutor:单线程化的线程池,只会使用唯一的线程池来执行任务。(缺点同Fixed Thread Pool)。
为什么Fixed Thread Pool和SingleThreadExecutor使用的队列是LinkedBlockingQueue,但是ScheduleThreadPool使用的是delayedWorkQueue?
因为LinkedBlockingQueue可以介绍大量的任务而不拒绝,适合在高负载的情况下作为缓冲;而delayedWorkQueue具有定时的特质,可以实现ScheduleThreadPool的定时或者延时执行的需求。
每个参数的最佳实践
核心线程数
I/O密集型:IO密集的任务,意味着使用CPU或者内存的频率比较低,CPU相对空闲,可以选择多开启一些核心线程数,一般可以为虚拟机核心线程数的2倍;
CPU密集型:计算密集型的任务,意味着使用CPU的频率很高,CPU比较忙碌,所以应该适当降低核心线程数,推荐值为虚拟机核心线程数+1;
更严谨的计算方法:最佳线程数=N(cpu 核心数)*(1+(WT(线程等待时间)/ST(线程计算时间)))。
其实解释一下就是在CPU核心数的基础上,线程等待的时间越长(线程越空闲),则设置的线程数越大,目的是为了提升CPU的利用率,不要让他空闲下来。
更真实的计算方法:给出的建议更多是参考,基于实际的业务去测试和摸索出最佳的配置。甚至可以使用配置中心设置参数来动态修改的方式来做。
最大线程数
一般可以设置设置为核心数的2-4倍,IO密集型则可以设置更多。
等待队列
电商场景中,通常使用有界的队列来避免内存的溢出,例如new LinkedBlockingQueue<>(2000),具体数值的大小要看处理的场景中的数据量。比如电商在处理商品的时候,总商品数量只有1000-2000条,那么设置2000条就比较合适。
拒绝策略
共有四种拒绝策略,其中三种是丢掉,一种是插队执行。
三种丢掉:
abortpolicy(默认):不处理新任务,并排除异常RejectedExecutionException;
discardPolicy:不处理新任务,直接丢掉;
discardOldestPolicy:添加新任务,丢掉队列中最旧的任务;
一种插队:
callerRunsPolicy:由调用线程来插队处理该任务【谁调用,谁处理】。缺点:降低新任务的提交速度,影响整体性能,增大处理的延迟;优点:尽最大可能得不丢弃请求,有着较高的处理完成度。
线程工厂
推荐使用guava的threadFactoryBuilder来进行创建,并带上该线程池中的基因命名(有业务意义的命名) ,这样方便进行问题的追踪。
以电商中使用的消息发送的线程池为例:
@Configuration
public class ThreadPoolConfig {
/**
* 这是一个用于处理商品库存和售价的定时任务,
且它被completableFuture的使用来组合分别执行处理库存和售价的结果。
**/
@Bean("sendMessageExecutor")
public Executor getSendMessageExecutor(){
//使用guava进行线程的命名,提高线程的识别性
String threadNamePrefix = "getSkuStockBatch-sendmessage";
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNameFormat(threadNamePrefix + "-%d")
.setDaemon(true).build();
//构建自定义的线程池(8核心16G cpu,IO密集型,考虑到其他线程的影响,给一个10的核心数,
//最大商品数量目前为299,设置为队列长度为300,最大核心线程数量为8*2)
ThreadPoolExecutor executor = new ThreadPoolExecutor(
10, 16, 3, TimeUnit.MINUTES, new LinkedBlockingQueue<>(300),
threadFactory,
new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
}
等待时间
对于超过最大核心线程数时,可以设置保持60秒的活跃时间。因为一般网络请求的超时时间也是60s。但是如果是定时任务的时间可以设置的长一点。
在电商中的具体实践
处理定时任务中库存异常的消息异步发送,这个例子不好,todo 抽空实现一个completableFuture的使用来组合分别执行处理库存和售价的结果的例子。
/**
* 描述: 异步处理消息发送
*
* @param skuId
* @param productName
* @return void
* @date 2023/3/9 13:05
*/
private void dealSendMessageAsync(Long skuId, String productName) {
//线程池前缀
log.info("dealSendMessageAsync异步处理消息发送");
//使用CompletableFuture线程池进行代码的异步运行
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
this.sendGoodsArrivalRemindMessage(skuId, productName);
}, sendMessageExecutor);
}
/**
* 同步erp库存时,如果现有库存量为0,erp的库存量大于0,则向订阅到货提醒且到货提醒信息打开的用户发送短信和站内信
*
* @param skuId
* @param productName
*/
private void sendGoodsArrivalRemindMessage(Long skuId, String productName) {
// 1.根据sku_id和到货提醒状态开启(1:开启)条件下查询关联的用户id
List<Long> goodsArrivalRemindUserIdList = userGoodsArrivalRemindMapper.getGoodsArrivalRemindUserIdList(skuId);
if (CollectionUtils.isEmpty(goodsArrivalRemindUserIdList)) {
return;
}
log.info("向开启该skuId:[{}]到货提醒的用户:{}发送短信:", skuId, goodsArrivalRemindUserIdList.toString());
// 2.遍历用户列表,查询message_send字段下到货信息提醒设置是否为1,1则发送站内信和短信
goodsArrivalRemindUserIdList.forEach(userId -> {
UserInfo userInfo = userInfoMapper.selectById(userId);
// 2.1 json序列化message_send
JSONObject jsonObject = JSON.parseObject(userInfo.getMessageSend());
if (ObjectUtils.isNotEmpty(jsonObject)) {
Integer goodsArrivalRemindVal = jsonObject.getInteger(ProductConstants.GOODS_ARRIVAL_REMIND);
if (ObjectUtils.isNotEmpty(goodsArrivalRemindVal) && goodsArrivalRemindVal.equals(1)) {
// 2.2获取手机号,并解密
String mobile = userInfo.getMobile();
if (ObjectUtils.isEmpty(mobile)) {
return;
}
try {
mobile = aesUtil.decrypt(mobile);
// 2.3 向用户发送短信
sendMobileMessage(mobile, productName);
// 2.4 向用户发送站内信
sendWebsiteMessage(userId, skuId, productName);
} catch (Exception e) {
log.error("向用户发送通知失败", e);
}
}
}
});
}