JAVA线程池解析
在上一篇线程基础中提到,在需要使用多线程执行任务时就可以通过创建额外的线程来实现,但是如果任务很多且执行任务较短,那么就需要频繁的创建与销毁线程,使得应用的处理效率大大降低。为了应对这种场景,线程池应运而生。
1 线程池原理
线程池的工作主要是管理线程(控制线程数量,线程复用),将接收到的任务放入任务队列,然后在线程创建后启动这些任务,当线程量达到最大线程量后,新的任务会在任务队列内阻塞等待,等待其他任务执行完毕再被取出进行线程复用执行。
1.1 线程复用
在上一篇线程基础中提到,每个Thread都通过start方法启动,在启动之后就会执行该类的run方法,run方法调用传递过来的Runnable的run方法执行业务逻辑,那么可以重写Thread类,在其start方法中循环的调用传递过来的Runnable,Runnable在任务队列中获取,任务队列被指定为一个阻塞队列,无法获取到Runnable时start方法是阻塞的。
1.2 线程池的组成
- 线程池管理器:用于创建并管理线程池。进行创建线程池,销毁线程池,添加新任务。
- 工作线程:线程池的线程。
- 任务接口:每个任务必须实现的接口,用于工作线程调度其运行。
- 任务队列:用于存放待处理的任务,提供一种缓存机制。
2 线程池的核心参数
ThreadPoolExecutor是JDK的基础线程池 ,JDK中的所有线程池都继承自ThreadPoolExecutor。通过查询ThreadPoolExecutor的构造方法可以获得线程池的核心参数,ThreadPoolExecutor的所有构造方法底层都是调用下面这个构造方法实现:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize:核心线程量,在默认的情况下核心线程是不会被销毁一直保持活跃状态的。但是可以通过allowCoreThreadTimeOut方法来设置使得核心线程也可以被超时销毁。
- maximumPoolSize:最大线程量,线程池允许创建的最大线程数量(包括核心线程和临时线程,临时限制在处于非活跃状态指定时间后会被销毁)。值得注意的是临时线程的创建时间。不是核心线程都处于运行状态来新任务时创建,而是核心线程都被占用,且队列被占满,此时来新任务才会创建临时线程。
- keepAliveTime与unit:可允许非活跃时间(即临时线程(和设置可销毁后的核心线程)在处于非活跃状态多久之后会被线程池销毁)
- workQueue:任务队列。一个阻塞队列,存放待执行任务。(参数限定必须为一个BlockingQueue)
- threadFactory:线程工厂。
- handler:拒绝策略。当线程池达到最大线程量且所以线程都被占用,任务队列已经满载的情况下或者线程池处于非Runing状态时新任务被提交时执行何种策略,可通过实现RejectedExecutionHandler接口自定义。jdk已提供4种策略。线程池默认为AbortPolicy策略:
①ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 拒绝执行异常,这里抛弃的是这个要加入线程池的任务 ②ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。这里抛弃的是这个要加入线程池的任务
③ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)(注意:这里抛弃的不是这个要加入线程池的任务,而是线程池等待队列的首位,也就是线程池下一个要执行的任务。这里的实现是直接用e.getQueue().poll(),然后调用e.execute( r ),r是要加入线程池的任务) ④ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
3 线程池的工作过程
- 线程池创建,池内没有任何线程,任务队列作为参数传入,但是此时即使任务队列内已经有任务,线程池也不会马上创建任务执行他们。
- 当调用execute或submit添加一个任务时,线程池执行如下逻辑:
① 如果当前存在的线程量小于核心线程量corePoolSize,则立即创建线程执行这个任务
② 如果当前存在的线程量大于等于核心线程量,则尝试将任务方法任务队列中,放入成功则结束
③ 如果任务队列已满,且当前存在的线程量小于最大线程量maximumPoolSize,则线程池创建临时线程处理这个任务
④ 如果任务队列已满,且当前存在的线程量等于(不会大于)最大线程量maximumPoolSize,则执行拒绝策略handler. - 一个线程执行完任务后会从任务队列中取出下一个任务继续执行。
- 如果任务队列中没有任务,线程则会处于阻塞状态。当处于阻塞状态的时长达到可允许非活跃时间(keepAliveTime与unit决定),就会查看当前线程量是否大于核心线程量corePoolSize,如果大于核心线程量那么这个线程就会被销毁掉。如果小于等于核心线程量,则会查看是否设置了允许核心线程超时(默认不允许),不允许则继续保留线程等待任务,允许则进行线程销毁。
- 线程执行过程中因为业务异常或中断退出,那么这个线程会在final块内执行processWorkerExit方法,这个方法会重新添加一个非核心线程到池子中(这个可能导致在任务队列没满的时间,线程量就大于了核心线程,比如我这边还没添加成功时,一个新任务被提交,添加了一个核心线程,然后这边在执行异常补充线程流程)
4 定时线程池如何实现定时以及周期执行
定时线程池不支持自定义阻塞任务队列,原因就是定时线程池需要这个特殊的阻塞队列DelayedWorkQueue来实现定时执行。DelayedWorkQueue是一个类似于DelayQueue的队列。
DelayQueue是一个延迟队列,队列中的元素需要实现Delay接口以确定到期时间。而Delay接口继承了Comparable,因此每个元素也需要实现compareTo方法。在compareTo方法内通过Delay接口定义的getDelay获得到期时间来确定比较大小,在加上DelayQueue内部通过一个PriorityQueue来维护存储数据。结合以上,DelayQueue就称为一个按到期时间排序的阻塞队列。取出元素时通过元素的getDelay来确定是否到期,到期则弹出,不到期则继续阻塞。
DelayedWorkQueue的内部元素被定义为RunnableScheduledFuture类型,RunnableScheduledFuture继承自ScheduledFuture,ScheduledFuture继承自Delayed。每一个任务被交到任务队列后都会被包装成RunnableScheduledFuture类型并设置到期时间放入DelayedWorkQueue任务队列中。这样定时线程池的任务队列就是一个按过期时间排序的一个延迟队列。线程池从DelayedWorkQueue中取出到期任务执行即实现了定时执行。而对于周期性任务(通过周期不为0判定),在任务执行完毕后会重新设置过期时间并载入任务队列,即实现了任务的周期性执行。
所有的定时线程池都是ScheduledThreadPoolExecutor实例,而ScheduledThreadPoolExecutor对外只提供了4个构造方法:
- public ScheduledThreadPoolExecutor(int corePoolSize)
- public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory) - public ScheduledThreadPoolExecutor(int corePoolSize,
RejectedExecutionHandler handler) - public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
可以看到,ScheduledThreadPoolExecutor只能指定核心线程量,线程工厂和拒绝策略。过期时间,最大线程量以及任务队列均不可以指定,任务队列不能指定在上文已经提出,是需为一个指定的延迟队列获取到期任务以实现定时执行。过期时间,最大线程量不能指定则是因为DelayedWorkQueue是一个初始容量为16的可扩容队列,且底层容器为数组,这意味着内存充足的情况下DelayedWorkQueue的容量可达Integer.MAX_VALUE,最大线程量的设置已经失去了意义,因此最大线程量以及针对临时线程的过期时间均失去了意义。
而ScheduledThreadPoolExecutor的构造方法则是直接调用的:
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory, handler)
ScheduledThreadPoolExecutor继承了ThreadPoolExecutor,super即为ThreadPoolExecutor的构造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler)
可以看到定时线程池的底层任务执行是通过基础线程池实现的。
因为阻塞队列的伪无限扩容,以及底层任务执行通过基础线程池实现也可以得出如果有任务到期而所有核心线程都被占用的话任务不会按期执行而是一直到等待有核心线程释放的结论,这回导致任务实际执行时间比预设时间慢。
5 线程池的状态
线程池共有5种状态:
- RUNNING:运行状态。接收新任务并处理任务队列中的任务
- SHUTDOWN:关闭状态。不接受新任务,但是处理任务队列任务
- STOP:中断状态。不接受新任务,也不处理队列任务,并中断正在处理的任务。
- TIDYING:清理状态。所有的任务已经结束,当前工作线程为0,此时会调用terminated(),terminated()状态的执行期间即为此状态
- TERMINATED:结束状态。terminated()方法执行完毕
5.1 状态转换条件
- RUNNING -> SHUTDOWN:调用shutDown()方法之后。或者是隐式的调用了protect级别的finalize()方法
- (RUNNING or SHUTDOWN) -> STOP:调用shutdownNow()方法之后
- SHUTDOWN -> TIDYING:当任务队列即线程池都为空,线程池内部调用terminated()
- STOP -> TIDYING:线程池为空,线程池内部调用terminated()
- TIDYING -> TERMINATED:terminated()方法执行执行完毕
6 JAVA中提供的线程池
JDK中Executors类提供了5种已经定义好的线程池,分别为以ThreadPoolExecutor为基础的基本线程池FixedThreadPool,SingleThreadExecutor,CachedThreadPool。以及以ThreadPoolExecutor的子类ScheduledThreadPoolExecutor为基础的定时线程池SingleThreadScheduledExecutor,ScheduledThreadPool。
在上面已经解析了ThreadPoolExecutor的核心参数,在此基础上就更加容易了解这些线程池的特点了:
6.1 基础线程池
- FixedThreadPool:固定线程量的线程池,即是一个核心线程和最大线程数都是入参的ThreadPoolExecutor
/**----------------FixedThreadPool-----------------*/
ExecutorService eService = Executors.newFixedThreadPool(1);// 固定线程量的线程池
/**return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());*/
eService = Executors.newFixedThreadPool(2, Executors.defaultThreadFactory());
/**public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);*/
// 根据构造可知其实一个核心线程和最大线程数都是入参的ThreadPoolExecutor
- SingleThreadExecutor:单线程线程池,即是一个核心线程和
最大线程数都是1
的ThreadPoolExecutor
/**----------------SingleThreadExecutor-----------------*/
eService = Executors.newSingleThreadExecutor();//
/**
- public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
*/
eService = Executors.newSingleThreadExecutor(Executors.defaultThreadFactory());
/**
- return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory));
*/
// newSingleThreadExecutor是一个只具有ExecutorService中方法,核心线程和最大线程数均为1的ThreadPoolExecutor线程池。
- CachedThreadPool:所有线程均为临时线程的缓存线程池。核心线程为0,最大线程量为Integer.MAX_VALUE,临时线程超时时间为60s,任务队列为SynchronousQueue,SynchronousQueue是一个无空间队列,这意味着一但有新任务来就会交给线程执行。如有有空闲的临时线程就用空闲的临时线程,如果没有就创建新的临时线程.临时线程空闲超过60s即被销毁
/**----------------CachedThreadPool-----------------*/
eService = Executors.newCachedThreadPool();//
/**
- return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
*/
eService = Executors.newCachedThreadPool(Executors.defaultThreadFactory());
/**
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
*/
// 可以看到这个同样是ThreadPoolExecutor来实现,核心线程为0,最大线程量为Integer.MAX_VALUE,临时线程保存60s,
// 且队列是一个无空间队列,这意味着一但有新任务来就会交给线程执行。
// 如有有空闲的临时线程就用空闲的临时线程,如果没有就创建新的临时线程.临时线程空闲超过60s即被销毁
6.2 定时线程池
- SingleThreadScheduledExecutor:单线程线程池,核心线程是1而
最大线程是Integer.MAX_VALUE的线程池。
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1));
}
- newScheduledThreadPool:核心线程是指定值而最大线程是Integer.MAX_VALUE的线程池。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
可以看到上面的线程最终都是创建一个ScheduledThreadPoolExecutor实例。
7 线程池的常用方法
8 停止定时任务
提交线程池定时任务时,会返回一个ScheduledFuture实例,调用这个实例的cancel(true)
方法即可停止对应的定时任务。
PS:
【JAVA核心知识】系列导航 [持续更新中…]
上篇导航:12:线程基础
下篇导航:14:JAVA线程池常用方法
欢迎关注…