我们常说的线程池,很大程度上指的就是ThreadPoolExecutor,他也是我们使用最为频繁的线程池。ThreadPoolExecutor的内部核心角色有三个:等待队列、线程和拒绝策略者:
线程池的内部很像一个工厂,等待队列就如同一个流水线,我们的任务就放在里面。 线程分为核心线程和非核心线程,他们就如同一个个的 “工人” ,从等待队列中获取任务进行执行。而如果这个“工厂”已经无法接受更多的任务,那么这个任务就会交给拒绝策略者去处理。
这里要特别注意的是,图中我分为两种线程是为了方便理解,而在实际中, 线程本身并没有核心与非核心之分 ,只有线程数大于核心线程数与小于核心线程数之分 。
例如核心线程数限制为3,但现在有4个线程正在工作:甲乙丙丁。当甲先执行完成时进入空闲状态时,因为总数超出设定的3,那么甲会被认为非核心线程被关闭。同理,如果丁先执行完成任务,则是丁被认为非核心线程被关闭。
ThreadPoolExecutor并不是把每个新任务都直接放到等待队列中,而是有一套既定的规则来执行每个新任务:
情况1:在线程数没有达到核心线程数时,每个新任务都会创建一个新的线程来执行任务。
情况2:当线程数达到核心线程数时,每个新任务会被放入到等待队列中等待被执行。
情况3:当等待队列已经满了之后,如果线程数没有到达总的线程数上限,那么会创建一个非核心线程来执行任务。
情况4:当线程数已经到达总的线程数限制时,新的任务会被拒绝策略者处理,线程池无法执行该任务。
这里我们可以发现对于线程池中的角色,有各种各样的数量上限,具体的有以下参数:
核心线程数corePoolSize:指定核心线程的数量上限;当线程数小于核心线程数,那么线程是不会被销毁的,除非通设置线程池的 allowCoreThreadTimeOut 参数为true,那么核心线程在等待 keepAliveTime 时间之后,就会被销毁。
允许核心线程被回收allowCoreThreadTimeOut :是否允许核心线程被回收。
最大线程数maximumPoolSize:指定线程总数的上限。线程总数=核心线层数+非核心线程数。当等待队列满了之后,新的任务会创建新的非核心线程来执行,直到线程数到达总数的上限。
线程闲置时间keepAliveTime:线程空闲等待该时间后会被销毁。非核心线程默认会被销毁,而核心线程则需要开发者自己设定是否允许被销毁。
等待队列BlockingQueue:存放任务的队列。我们可以在创建队列的时候设置队列的长度。一般使用的队列有LinkedBlockingQueue、ArrayBlockingQueue、SychronizeQueue、PriorityBlockingQueue等,前两者是普通的阻塞队列,最后一个是优先阻塞队列,剩下的一个是没有容量的队列。
拒绝策略者RejectExecutionHandler :线程池无法处理的任务就会交给他处理。
通过设置线程池的这些参数可以让线程池拥有完全不同的特性,来适应不同的情景。通常我们在ThreadPoolExecutor的构造方法中,会传入这些参数:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { ... }基本都是我们上面介绍过的参数,有两个参数我们还没见过:
时间单位unit:给前面的keepAliveTime指定时间单位。
线程工厂threadFactory: 线程池会通过线程工厂来创建新的线程,主要是给线程指定一个有意义的名字。
当然,我们也可以不用全部指定上面的参数,ThreadFactory和RejectedExecutionHandler不指定可以使用默认的参数。
配置ThreadPoolExecutor经过概述,我们对ThreadPoolExecutor的参数以及内部结构已经非常清楚了,接下来探讨一下如何合理地进行配置。
首先是核心线程数。核心线程太少,线程之间会互相争夺cpu资源,多个线程之间进行时间片轮换,大量的线程切换工作会增大系统的开销;核心线程太少,会导致一些cpu核心在挂起等待阻塞操作,没法充分利用cpu资源;核心线程数的配置就是要均衡这两个特点。
关于核心线程数的配置,有一个广为人知的默认规则:
cpu密集型任务,设置为CPU核心数+1;
IO密集型任务,设置为CPU核心数*2;