文章目录
一、线程池的参数配置
1.corePoolSize核心线程数选择
配置线程数量之前,首先要看任务的类型是 IO密集型,还是CPU密集型?
什么是IO密集型?比如:频繁读取磁盘上的数据,或者需要通过网络远程调用接口。
什么是CPU密集型?比如:非常复杂的调用,循环次数很多,或者递归调用层次很深等。
IO密集型配置线程数经验值是:2N,其中N代表CPU核数。
CPU密集型配置线程数经验值是:N + 1,其中N代表CPU核数。
如何获取CPU核数?
int availableProcessors = Runtime.getRuntime().availableProcessors();
2.workQueue工作队列(阻塞队列)选择
线程池中常用的阻塞队列有三种
BlockingQueue<Runnable> workQueue = null;
workQueue = new SynchronousQueue<>();//无缓冲的等待队列
workQueue = new ArrayBlockingQueue<>(5);//基于数组的先进先出队列
workQueue = new LinkedBlockingQueue<>();//基于链表的先进先出队列
如何选择?看看三者的区别
SynchronousQueue是一个不存储元素的阻塞队列,适合传递性场景,只是负责把父线程提交的任务直接交给线程池线程处理。也就是说提交的任务数超过最大线程数就会执行拒绝策略
ArrayBlockingQueue底层是用数组实现的有界阻塞队列,因为需要传初始值(如果传Integer最大值,也类似于无界了)。队列按照先进先出的原则对元素进行排序
LinkedBlockingQueue底层是用链表实现的有界阻塞队列,如果不传初始化值为Integer最大值,也是先进先出对元素进行排序
一般选择建议选择有界队列,因为如果任务特别多,核心线程处理不过来,会将任务都放到工作队列中,此时最大线程数已经没有意义了。如果控制不好会导致OOM
那么ArrayBlockingQueue和LikendBlockingQueue选择哪一个?
从底层实现来看LikendBlockingQueue需要维护一个个Node对象,需要额外的内存消耗。并且在生产和消费的时候,需要创建Node对象进行插入或移除,大批量数据的系统中,其对于GC的压力会比较大。
ArrayBlockingQueue只是维护final Object[] items;一个数组。在生产和消费的时候,是按照索引对数据插入或移除的,不会产生或销毁任何额外的对象实例
综合来说,可以选择ArrayBlockingQueue。
3.阻塞队列长度和最大线程数选择
这个问题网上大多回答是根据项目配置、响应时间要求来判断,或者根据公式计算。我觉得还是要根据具体项目来选择,比如可能要求在一定的响应时间内完成需求。
拿具体项目举例吧
项目需求
1.从项目中将客户数据导出pdf,然后压缩,下载下来
2.使用线程池子线程导出每个客户的pdf,压缩使用主线程
3.每个客户导出pdf的时间大概在1.5s左右,一组客户平均在60人,最大在150人
4.响应时间要在3s内完成
那么阻塞队列长度和最大线程数应该怎么设置?
因为响应时间在3s内,每一个pdf生成需要1.5s,那么可以将一组客户分为两组执行,一半放到阻塞队列,一半直接创建非核心线程执行。按组数人最大的进行分组,那么最大线程数和阻塞队列数对半分。还需要考虑主线程执行时间,那么可以设置非核心线程数大一点。推导的最后结果就是最大线程数可以设置成100,阻塞队列长度也可以设置成100。记得一定要回收非核心线程,配置keepAlivedTime。
4.拒绝策略选择
JDK提供了四种拒绝策略
- AbortPolicy:直接丢弃新任务,抛出异常
- DiscardPolicy:直接丢弃掉,不会抛出异常
- DiscardOldestPolicy:丢弃时间最久的任务。一般是队列最前面的任务
- CallerRunsPolicy:交给主线程去执行
当然也可以自定义拒绝策略,如果你的任务不能被拒绝的话,可以让任务重回队列,重新执行或者交给主线程执行。在下面例子中会写到。
二、SpringBoot环境下配置线程池
1.线程池配置
一般在Spring环境下,我们可以将ThreadPoolExecutor作为一个Bean交给Spring管理。配置如下
//1.yml文件配置
demo:
thread:
coreSize: 8
maxSize: 100
keepAliveTime: 60
queueLength: 100
//2.读取yml文件配置
@ConfigurationProperties(prefix = "demo.thread")
@Data
public class ThreadPoolConfigProperties {
private Integer coreSize;
private Integer maxSize;
private Integer keepAliveTime;
private Integer queueLength;
}
//3.配置ThreadPoolExector的bean
@EnableConfigurationProperties(ThreadPoolConfigProperties.class)
@Configuration
public class MyThreadConfig {
@Bean
public ThreadPoolExecutor threadPoolExecutor(ThreadPoolConfigProperties pool){
return new ThreadPoolExecutor(
pool.getCoreSize(),
pool.getMaxSize(),
pool.getKeepAliveTime(),
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(pool.getQueueLength()),
Executors.defaultThreadFactory(),
new RejectedExecutionHandler() {
@SneakyThrows
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
while (executor.getQueue().offer(r,5,TimeUnit.SECONDS)){
break;
}
}
}
);
}
}
//4.使用,直接测试即可
@Autowired
private ThreadPoolExecutor executor;
@RequestMapping("/threadtest")
public void test(){
for (int i = 0;i<100;i++){
executor.execute(()->{
System.out.println("sdf");
});
}
}
学习下两个注解
@ConfigurationProperties和@EnableConfigurationProperties。两者的关系简单点说就是@EnableConfigurationProperties让@ConfigurationProperties注解的类的生效,并且将@ConfigurationProperties注解的bean信息作为spring的环境bean,可以直接取出数据
2.线程池的监控
线程池让线程得以复用、得以管理,但是线程池一旦使用不当可能会造成服务宕机、内存溢出等问题,所以我们可以监控下线程池。线程池提供一些方法可以获取相关的信息
拿到这些信息有什么用呢?还记得之前的SpringBoot Actuator吗,可以通过该机制将线程池的运行状态暴露出去,通过prometheus采集然后展示就可以了。
参考文章:
常见的8种拒绝策略:https://zhuanlan.zhihu.com/p/142254564
线程池企业级应用:https://blog.csdn.net/AlbenXie/article/details/105292727