线程池的描述
前言
多线程的使用可以提升程序运行的时间效率,同时也发挥多核CPU的计算能力的优势,从而提升系统的吞吐量和性能。然而,如果不合理地使用多线程,就会造成CPU的上下文切换频繁,反而因为对CPU和内存耗尽造成对性能产生不利的影响,从而拖垮了系统。故我们需要对线程的使用有个限度,限制运行线程的数量的范围。
1.线程池的定义
线程池是一种存放一定数量的活跃线程的对象,在需要使用线程时,从其中拿出一个活跃线程,运行完毕之后把对应的线程放回。
2.线程池的优势
使用线程池,可以实现对活跃线程的复用来提高线程的利用率,从而避免频繁创建和销毁线程,节约了线程的运行时间。
3.线程池的作用
降低资源消耗:通过对活跃线程的复用减少频繁创建和销毁线程的时间,提升内存和CPU的利用率。
响应速度快:任务到达时,无需创建可立即执行。
可管理性:因为一个线程的创建和销毁占用很大的CPU和内存,若没有管理则造成资源的消耗,所以可以控制对应线程的数量来提升CPU和内存的利用率。使用线程池可以进行统一的分配、调优和监控。
可扩展性:线程池有很强的可扩展性,允许开发人员增加更多功能。比如使用可定时线程池来定时执行对应的线程。
4.线程池创建方式与执行原理
4.1jdk线程池框架Executor
jdk提供了框架Executor来表示线程池,方便了开发人员的使用。该框架位于java.util.concurrent包中,在java 5开始出现的。核心成员如图4.1所示。
图4.1 线程池成员
上图中的ThreadPoolExecutor是线程池的通用具体类,实现Executor接口,ScheduledThreadPoolExecutor为可定时的线程池是ThreadPoolExecutor子类。因此通过Executor接口,任何Runnable对象可以被ThreadPoolExecutor调用。
4.2jdk自带的线程池创建工具类Executors
jdk也提供了工具类Executors来创建线程池,这些线程池的底层原理都是通过ThreadPoolExecutor封装的。具体有如下的方法:
Executors.newFixedThreadPool(int nThreads);可定长度:返回固定线程数量的线程池,该线程池线程数量始终不变。若有空闲线程,则立即执行已提交的线程;反之,则新提交的任务被暂存到一个备胎任务队列中,等到线程池空闲时,便处理该备胎队列的任务。
Executors.newSingleThreadExecutor(); 单例:只有一个线程的线程池。若多于一个任务被提交到该线程池,则新提交的任务被暂存到一个备胎任务队列中,等到线程池空闲时,便处理该备胎队列的任务。
Executors.newCachedThreadPool(); 可缓存线程池:该线程池可根据实际情况来调整线程的数量,故它的活跃线程数目不确定,但若有空闲可复用,则会优先使用可复用的线程。反之,则会创建新的线程。在所有线程执行完毕时,将返回线程池进行复用。
Executors.newSingleThreadScheduledExecutor(); 单例可定时的:在单例线程池的基础上加入了在给定的时间执行任务的功能。
Executors.newScheduledThreadPool(int corePoolSize) ; 可定时:返回可定时线程池的对象,在可定长度线程池基础上扩展加入了在给定时间执行任务的功能。
4.3为何不建议使用Executors工具类创建线程池
在阿里巴巴的开发手册中,不建议使用Executors来创建线程池,具体的截图如图4.2所示:
图4.2 阿里不建议使用Executors
正如前言所说的,创建一个线程是很耗费时间及硬件资源的,如果超过硬件可允许的最大线程长度,就会造成内存溢出的故障。故建议通过自定义ThreadPoolExecutor类的对象来创建线程池。
4.4 线程池核心类ThreadPoolExecutor原理
图4.3 ThreadPoolExecutor核心构造方法
4.1.1 线程池核心参数
如图4.3所示,ThreadPoolExecutor的构造方法定义了核心线程数、最大线程数、超过核心线程数的多余线程存活时间、存活时间单位,被提交但尚未执行的任务队列、线程工厂、拒绝策略处理器这几个参数。具体说明如下:
corePoolSize:核心线程数量 一直正在保持运行的线程数量。
maximumPoolSize:最大线程数,线程池允许创建的最大线程数。
keepAliveTime:超出corePoolSize后创建的线程的存活时间。
unit:keepAliveTime的时间单位。
workQueue:任务等待队列,用于保存待执行的任务,也就是被提交但尚未执行的任务队列。
threadFactory:线程工厂,线程池内部创建线程所用的工厂。
handler:任务无法执行时的处理器,用于执行拒绝策略。
4.1.2 线程池拒绝策略
如果正在执行的任务队列满了(也就是正在运行的线程数超过了最大线程数),就需要执行对应的拒绝策略。拒绝策略是实现了RejectedExecutionHandler接口。java的线程池常见的拒绝策略如下:
AbortPolicy 丢弃任务,抛运行时异常
CallerRunsPolicy 在线程池未关闭时,直接调用run方法执行被丢弃的任务
DiscardPolicy 丢弃无法处理的任务,什么都不会发生
DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务
实现RejectedExecutionHandler接口,可自定义处理器
4.1.3 线程池运行原理
在有一个任务提交时,若线程池实际线程数不大于核心线程数,则直接复用线程执行。当实际的线程数大于核心线程数,小于最大线程数时,把任务放入等待队列,若等待队列已满,则创建新的线程执行任务。当实际线程数大于最大线程数的时候,线程池就执行拒绝策略。线程池提交线程时通常使用executor(Runnable r)方法来提交Runnable任务。具体的原理图如图4.4所示。
图4.4 线程池执行原理
4.1.4 线程池的状态
RUNNING:线程池能够接受新任务,以及对新添加的任务进行处理。
SHUTDOWN:线程池不可以接受新任务,但是可以对已添加的任务进行处理。
STOP:线程池不接收新任务,不处理已添加的任务,并且会中断正在处理的任务。
TIDYING:当所有的任务已终止,ctl记录的"任务数量"为0,线程池会变为TIDYING状态。当线程池变为TIDYING状态时,会执行构造函数5.terminated()。terminated()在ThreadPoolExecutor类中是空的,若用户想在线程池变为TIDYING时,进行相应的处理;可以通过重载terminated()函数来实现。
TERMINATED:线程池彻底终止的状态。
4.1.5 线程池的等待队列
在线程池工作的线程数大于核心线程数且不大于最大线程数时,把提交的线程放入等待队列workingQueue里。参数workingQueue是BlockingQueue接口的实现类。当workingQueue满了时,则创建线程执行提交的任务。具体的等待队列实现有如下几个:
ArrayBlockingQueue:有界队列,基于数组结构,按照队列FIFO原则对元素排序;
LinkedBlockingQueue:无界队列,基于链表结构,按照队列FIFO原则对元素排序,Executors.newFixedThreadPool()使用了这个队列; 无界默认是Integer.MAX_VALUE,有界则是 可以自己定义
SynchronousQueue:同步队列,该队列不存储元素,每个插入操作必须等待另一个线程调用移除操作,否则插入操作会一直被阻塞,Executors.newCachedThreadPool()使用了这个队列;
PriorityBlockingQueue:优先级队列,具有优先级的无限阻塞队列。
4.5 线程池参数如何合理设置
具体可以根据CPU密集与io密集配置。
CPU密集,线程不会阻塞,一直在运行,线程代码非常快结束,则最大线程数配置与
CPU核数相当就可以了。
Io密集,比如读取io、导致当前线程有可能阻塞,或者线程执行代码非常耗时,则最大线程数配置为CPU核数*2即可。
5.线程池在项目中的应用
在真实的项目中,线程池需要整合对应的spring框架来统一配置,从而实现解耦。正如前面所说的,如果直接创建线程池,就容易造成硬件资源的不合理分配从而造成系统的崩溃。在spring框架中加入对应的配置类来统一配置线程池,同时业务层使用@Async注解标记需要异步执行的方法,其底层原理是AOP。具体的配置方法如下:
@Configuration
@EnableAsync
public class ExecutorConfig {
@Value("${thread.maxPoolSize}")
private Integer maxPoolSize;
@Value("${thread.corePoolSize}")
private Integer corePoolSize;
@Value("${thread.keepAliveSeconds}")
private Integer keepAliveSeconds;
@Value("${thread.queueCapacity}")
private Integer queueCapacity;
@Bean
public ThreadPoolTaskExecutor asyncExecutor(){
ThreadPoolTaskExecutor taskExecutor=new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(corePoolSize);//核心数量
taskExecutor.setMaxPoolSize(maxPoolSize);//最大数量
taskExecutor.setQueueCapacity(queueCapacity);//队列
taskExecutor.setKeepAliveSeconds(keepAliveSeconds);//存活时间
taskExecutor.setWaitForTasksToCompleteOnShutdown(true);//设置等待任务完成后线程池再关闭
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());//设置拒绝策略
taskExecutor.initialize();//初始化
return taskExecutor;
}
}
图5.1 spring线程池配置
@Async
public void saveOrUpdateDtos(List<SecSimExcelDTO> dtos, FileImportRecord fileImportRecord) {
int maxDealNum = 5000;
try {
insertDtos(dtos, fileImportRecord, maxDealNum);
fileImportRecord.setImportStatus("03");
fileImportRecord.setErrorInfo(" ");
log.info("import end!");
} catch (Exception e) {
log.error("here is an exception", e);
fileImportRecord.setImportStatus("05");
fileImportRecord.setErrorInfo(e.getClass().getName() + "-" + e.getMessage().replace(":", " ").replace(";", " "));
}
fileImportRecord.setImportEndTime(LocalDateTime.now());
fileImportRecord.setRecTime(LocalDateTime.now());
fileImportRecordMapper.updateByPrimaryKeySelective(fileImportRecord);
}
图5.2 业务逻辑层上面使用@Async注解的方式
注意:@Async注解不得出现在与@Controller有关的类里,这样容易导致@Controller的url不能注册到springmvc的容器里造成@Async注解失效。