Java线程池介绍
一、线程池的概念
线程池(英语:thread pool):一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。线程数一般取cpu数量+2比较合适,线程数过多会导致额外的线程切换开销。
二、线程池的状态
线程池共有5种状态,分别是RUNNING、SHUTDOWN、STOP、TIDYING和TERMINATED:
RUNNING:线程池处于该状态时,可以接收新的任务、运行等待队列中的任务。
状态触发:线程池被创建。
SHUTDOWN:线程池处于该状态时,不会接收新的任务,但会执行等待队列中的任务。
状态触发:RUNNING状态下,调用shutdown()方法。
STOP:线程池处于该状态时,不会接收新的任务,不会执行等待队列中的任务,并且会中断正在处理中的任务。
状态触发:RUNNING/SHUTDOWN状态下,调用shutdownNow()方法。
TIDYING:线程池所有的任务已终止,ctl记录的任务数为0时,则会处于该状态。
状态触发:
(1)当线程池在SHUTDOWN状态下,阻塞队列为空且线程池中执行的任务也为空;
(2)当线程池在STOP状态下,线程池中执行的任务为空。
注:当线程池处于TIDYING状态时,会执行terminated()方法终止线程池。
TERMINATED:线程池彻底终止,则会处于该状态。
状态触发:TIDYING状态下,terminated()方法执行完毕。
5种状态之间的转换如下图所示:
三、线程池相关类
接口:
Executor:仅定义了execute(Runnable command)执行任务的方法;
ExecutorService:继承自Executor接口,新增了任务的管理方法;
ScheduledExecutorService:继承自ExecutorService接口,新增了定时处理任务的方法。
类:
AbstractExecutorService:实现了ExecutorService接口,提供了各方法的默认实现;
ThreadPoolExecutor:继承自AbstractExecutorService抽象类,线程池的实现类;
ScheduledThreadPoolExecutor:实现了ScheduledExecutorService接口,继承自ThreadPoolExecutor类,因此可以将其看成是基本线程池类的扩展版,提供了线程池任务的延迟/定时处理功能;
DelegatedExecutorService:Executors工具类中用到,不关心;
FinalizableDelegatedExecutorService:Executors工具类中用到,不关心;
DelegatedScheduledExecutorService:Executors工具类中用到,不关心;
ForkJoinPool:fork/join框架中用到,不关心;
ThreadPerTaskExecutor:fork/join框架中用到,不关心。
线程池的重点实现类有两个:ThreadPoolExecutor和ScheduledThreadPoolExecutor。
ThreadPoolExecutor
ThreadPoolExecutor的构造函数有以下4个:
其中前3个构造函数,都是调用的最后一个构造函数,该函数存在7个参数,各参数的说明如下:
corePoolSize
类型:int
说明:线程池核心执行线程数,该值不能小于0。所谓核心线程,指的是线程池中的常驻存活线程数量,无论是否有任务需要执行,即使线程为空闲状态,也不会关闭。
maximumPoolSize
类型:int
说明:线程池最大执行线程数,该值必须大于0。当线程池中的核心线程全部在工作,并且等待队列已满,则线程池会创建新的线程处理任务,该参数限定的就是核心线程数+额外创建的线程数总和,换句话说,无论如何,线程池里可执行的线程数量不可能超出该参数设置的数量。
keepAliveTime
类型:long
说明:空闲线程等待工作的最大存活时间,该值不能小于0。空闲线程指的是除了核心线程之外,线程池为了处理任务而额外创建的其他线程,当任务处理完毕,额外创建的线程处于空闲状态,即为空闲线程。此时若设置了该参数,则空闲线程会在指定时间内存活,超出时间后线程会被销毁。当该参数设置为0时,代表空闲线程立刻销毁。
注:默认情况下,核心线程是不会受到该参数的影响的,但是可以通过调用allowCoreThreadTimeOut(boolean value)方法,传入的布尔值用于设置是否允许核心线程也通过此参数进行线程的超时销毁。若传入value值为true的话,则keepAliveTime参数必须要大于0。
unit
类型:TimeUnit
说明:空闲线程存活的时间单位,与keepAliveTime配合使用,不能为空。类型TimeUnit为枚举类,一般直接根据情况使用枚举值即可,如TimeUnit.SECONDS、TimeUnit.MINUTES等。
workQueue
类型:BlockingQueue<Runnable>
说明:任务队列,不能为空。该队列用于保存等待执行的任务。从队列元素数量的有限与无限来区分,任务队列可分为有界队列和无界队列,前者可保存的元素是有限的,后者可以看成没有元素数量的限制(实际不能超过Integer.MAX_VALUE)。
实际开发中更推荐使用有界队列,因为使用无界队列,在任务添加的频率比任务处理完成的频率要高的时候,队列保存的任务数量会一直累加,直到引发OOM异常。
常用的队列有以下几种:
ArrayBlockingQueue
该队列为有界队列,使用数组保存元素。该队列存在3个构造函数:
public ArrayBlockingQueue(int capacity):可传入容量参数,默认构建非公平(FIFO)的队列;
public ArrayBlockingQueue(int capacity, boolean fair):可传入容量和锁公平参数;
public ArrayBlockingQueue(int capacity, boolean fair, Collection<? extends E> c):可传入容量、锁公平和初始化的元素集合,该集合会被遍历,元素依次添加到队列中。
LinkedBlockingQueue
该队列既可以是有界队列,也可以是无界队列,通过调用不同的构造函数来区分。使用链表结构保存元素。该队列存在3个构造函数:
public LinkedBlockingQueue():无参,构建队列,初始化容量为Integer.MAX_VALUE;
public LinkedBlockingQueue(int capacity):传入容量参数,构建指定容量的队列;
public LinkedBlockingQueue(Collection<? extends E> c):传入元素集合,构建容量为Integer.MAX_VALUE的队列,并将集合的元素依次遍历添加到队列中。
SynchronousQueue
SynchronousQueue没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素。因此在使用该队列时,通常要求maximumPoolSizes设置为无界(Integer.MAX_VALUE)。该队列存在2个构造函数:
public SynchronousQueue():创建默认非公平的队列;
public SynchronousQueue(boolean fair):传入fair参数,队列指定公平性。
threadFactory
类型:ThreadFactory
说明:线程工厂,线程池中的线程均通过该工厂创建。ThreadFactory类是一个函数式接口,仅有一个方法:newThread(Runnable r),返回Thread类型的线程。
java提供了一个默认的实现类DefaultThreadFactory。
handler
类型:RejectedExecutionHandler
说明:拒绝策略,在使用线程池并且使用有界任务队列的时候,如果线程池中的线程到达最大数量,并且等待队列已满,则新任务添加到线程池的时候就会触发该策略进行处理。RejectedExecutionHandler类也是一个函数式接口,仅有一个方法:rejectedExecution(Runnable r, ThreadPoolExecutor executor)。
java线程池默认提供了以下4种拒绝策略:
AbortPolicy
该策略是线程池的默认策略,对于新的任务,直接拒绝执行并且抛出异常。
注:使用该策略时,建议在调用execute方法的地方使用try-catch捕获该异常,否则异常抛出,后续任务无法继续添加到线程池中。
CallerRunsPolicy
该策略会使用调用线程池的Thread线程处理任务。
DiscardOldestPolicy
该策略会将最早加入任务队列的任务移除,同时将新的任务添加到任务队列。
DiscardPolicy
该策略不做任何处理,默认丢弃任务,并且不会抛出异常。
此外我们可自定义实现拒绝策略,只要实现RejectedExecutionHandle,重写rejectExecution方法即可。
ThreadPoolExecutor执行线程的方法为execute(Runnable command),传入一个Runnable对象即可。
ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor的构造函数有以下4个:
实际上ScheduledThreadPoolExecutor的构造函数,调用的就是父类ThreadPoolExecutor的构造方法,只不过在初始化的时候显示指定了某些参数。
ScheduledThreadPoolExecutor主要用于定时/延迟执行线程,主要方法如下:
前两个schedule()方法,第一个参数分别对应无返回值(Runnable)和有返回值(Callable)的线程,返回的结果封装在返回类型ScheduledFuture对象中,使用get()方法取得;第二个long类型参数为线程延迟执行的时间值;第三个参数为延迟执行的时间单位。
后两个方法用于定期执行某个线程,这两个方法的参数一模一样,
Runnable command, long initialDelay, long period, TimeUnit unit
分别代表Runnable线程对象、初始化执行延迟、定时执行频率和时间单位。二者的不同之处在于:
scheduleAtFixedRate方法的周期执行计算是从上一个线程刚执行的那个时间段开始,经过period延迟后,会直接执行下一个线程,若上一个线程未执行完毕,则会等待其执行完毕后立刻执行(此时已经超出了period延迟,所以会立刻执行)。该方法优先保证任务执行的频率;
scheduleWithFixedDelay方法的周期执行计算是从上一个线程执行完毕后开始,经过period延迟后再执行下一个线程,换句话说,调用scheduleWithFixedDelay方法,定时执行的每个线程之间的时间间隔为period。该方法优先保证任务执行的间隔。
四、线程池的工作流程
当一个任务通过execute(Runnable)方法欲添加到线程池时,分为以下几个阶段:
第一阶段(判断核心线程数)
此时线程池中的线程数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也会创建新的线程来处理被添加的任务,直到线程数量到达corePoolSize,即率先创建所有的核心线程;
第一阶段过后,此时线程池的线程数量从初始化的0逐步增加至上限corePoolSize,等待队列中元素数量为0;
第二阶段(判断等待队列数)
此时线程池中的线程数量等于 corePoolSize,若等待队列 workQueue未满,那么任务将被放入等待队列,直到任务数量到达等待队列的最大size;
第二阶段过后,此时线程池的线程数量保持不变,依旧是corePoolSize,等待队列中元素数量从0逐步增加至上限(如果为有界队列的话);
第三阶段(判断最大线程数)
此时线程池中的线程数量等于corePoolSize,缓冲队列workQueue满,若线程池中的线程数量小于maximumPoolSize,则创建新的线程来处理被添加的任务。
第三阶段过后,此时线程池的线程数量从corePoolSize逐步增加至上限maximumPoolSize,等待队列元素基本维持在上限(实际中因为任务处理完成,队列的元素数量会不断变化,这里假设任务处理速度慢);
第四阶段(执行拒绝策略)
此时线程池中的线程数量等于maximumPoolSize,缓冲队列workQueue满,程序将通过 handler所指定的拒绝策略来处理此任务。
因此可以看出处理任务的优先级为:核心线程corePoolSize、任务队列workQueue、最大线程maximumPoolSize,若三者都满了,则使用handler处理被拒绝的任务。
五、Executors工具类
Java默认提供了一个创建线程池的工具类Executors,使用该类,可以很便捷地创建所需的各式各样线程池。Executors常用创建线程池的方法如下:
实际上Executors的这些静态方法,调用的还是ThreadPoolExecutor的构造函数,区分在与参数的不同,如等待队列、拒绝策略等。
注:不建议使用Executors创建线程池。原因有以下几点:
1、过度依赖这样的线程池创建方式,不利于开发者对于线程池的核心参数理解;
2、OOM风险:
无界队列,队列大小为Integer.MAX_VALUE,过多任务的堆积可能会引起OOM;
线程数无限,maximumPoolSize为Integer.MAX_VALUE,过多线程的启动可能会引起OOM。