为什么要用线程池?
不用线程池的缺陷
若采用”为每个任务分配一个线程”的方式会存在一些缺陷,尤其是当需要创建大量线程时:
线程生命周期的开销非常高
资源消耗
稳定性
用线程池的好处
任务是一组逻辑工作单元,线程则是使任务异步执行的机制。当存在大量并发任务时,创建、销毁线程需要很大的开销,运用线程池可以大大减小开销
线程池框架
一、Executor 接口简介
Executor接口是Executor框架的一个最基本的接口,位于java.util.concurrent包中,Executor框架的大部分类都直接或间接地实现了此接口。
三个实现类:AbstractExecutorService(默认实现类),ScheduledThreadPoolExecutor,,ThreadPoolExecutor
源码:
1 | public interface Executor { |
只有一个方法:
void execute(Runnable command): 在未来某个时间执行给定的命令。该命令可能在新的线程、已入池的线程或者正调用的线程中执行,这由 Executor 实现决定。
二、ExecutorService 接口简介
ExecutorService 是一个继承自Executor的接口,提供了管理终止的方法,以及可为跟踪一个或多个异步任务执行状况而生成Future 的方法。
源码:
1 | public interface ExecutorService extends Executor { |
源码解读:
- 从Executor 接口中继承了不跟踪异步线程,没有返回的 execute 方法:
void execute(Runnable command):
在未来某个时间执行给定的命令。该命令可能在新的线程、已入池的线程或者正调用的线程中执行,这由 Executor 实现决定。
- 扩展的跟踪异步线程、返回Future 接口的实现类的方法:
Future<?> submit(Runnable task):
提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。该 Future 的 get 方法在成功 完成时将会返回 null。
提交一个 Runnable 任务用于执行,并返回一个表示该任务的 Future。该 Future 的 get 方法在成功完成时将会返回给定的结果。
提交一个返回值的任务用于执行,返回一个表示任务的未决结果的 Future。该 Future 的 get 方法在成功完成时将会返回该任务的结果。如果想立即阻塞任务的等待,则可以使用 result = exec.submit(aCallable).get(); 形式的构造
执行给定的任务,当所有任务完成时,返回保持任务状态和结果的 Future 列表。返回列表的所有元素的 Future.isDone() 为 true。注意,可以正常地或通过抛出异常来终止已完成 任务。如果正在进行此操作时修改了给定的 collection,则此方法的结果是不确定的。
超时等待,同上。
与 invokeAll的区别是,任务列表里只要有一个任务完成了,就立即返回。而且一旦正常或异常返回后,则取消尚未完成的任务。
超时等待,同上。
boolean awaitTermination(long timeout,TimeUnit unit) throws InterruptedException:
一直等待,直到所有任务完成。请求关闭、发生超时或者当前线程中断,无论哪一个首先发生之后,都将导致阻塞,直到所有任务完成执行,或者超时时间的到来如果此执行程序终止,则返回 true;如果终止前超时期满,则返回 false
- 管理生命周期
void shutdown():
平缓的关闭线程池。线程池停止接受新的任务,同时等待已经提交的任务执行完毕,包括那些进入队列还没有开始的任务。shutdown()方法执行过程中,线程池处于SHUTDOWN状态。
List
即关闭线程池。线程池停止接受新的任务,同时线程池取消所有执行的任务和已经进入队列但是还没有执行的任务。shutdownNow()方法执行过程中,线程池处于STOP状态。shutdownNow方法本质是调用Thread.interrupt()方法。但我们知道该方法仅仅是让线程处于interrupted状态,并不会让线程真正的停止!所以若只调用或只调用一次shutdownNow()方法,不一定会让线程池中的线程都关闭掉,线程中必须要有处理interrupt事件的机制。boolean isShutdown(): 如果线程池停止完成返回true
boolean isTerminated():
如果关闭后所有任务都已完成,则返回 true。注意,除非首先调用 shutdown 或 shutdownNow,否则 isTerminated 永不为 true。
三、ScheduledExecutorService接口简介
ScheduledExecutorService描述的功能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。这包括延迟时间一次性执行、延迟时间周期性执行以及固定延迟时间周期性执行等。当然了继承ExecutorService的ScheduledExecutorService拥有ExecutorService的全部特性。
四、AbstractExecutorService(实现了ExecutorService接口的抽象类)
AbstractExecutorService对ExecutorService的执行任务类型的方法提供了一个默认实现。这些方法包括submit,invokeAny和InvokeAll。
注意的是来自Executor接口的execute方法是未被实现,execute方法是整个体系的核心,所有的任务都是在这个方法里被真正执行的,因此该方法的不同实现会带来不同的执行策略。
详细请移步至源码AbstractExecutorService解读
五、ThreadPoolExecutor类
构造器及相关属性
1 | public ThreadPoolExecutor(int paramInt1, int paramInt2, long paramLong, TimeUnit paramTimeUnit, |
corePoolSize :线程池的核心池大小,在创建线程池之后,线程池默认没有任何线程。当有任务过来的时候才会去创建线程执行任务。换个说法,线程池创建之后,线程池中的线程数为0,当任务过来就会创建一个线程去执行,直到线程数达到corePoolSize 之后,就会被到达的任务放在队列中。(注意是到达的任务)。换句更精炼的话:corePoolSize 表示允许线程池中允许同时运行的最大线程数。
如果执行了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程。
maximumPoolSize :线程池允许的最大线程数,他表示最大能创建多少个线程。maximumPoolSize肯定是大于等于corePoolSize。
keepAliveTime :表示线程没有任务时最多保持多久然后停止。默认情况下,只有线程池中线程数大于corePoolSize 时,keepAliveTime 才会起作用。换句话说,当线程池中的线程数大于corePoolSize,并且一个线程空闲时间达到了keepAliveTime,那么就是shutdown。
Unit:keepAliveTime 的单位。
workQueue :一个阻塞队列,用来存储等待执行的任务,当线程池中的线程数超过它的corePoolSize的时候,线程会进入阻塞队列进行阻塞等待。通过workQueue,线程池实现了阻塞功能
threadFactory :线程工厂,用来创建线程。
handler :表示当拒绝处理任务时的策略。
execute执行策略
结合上面的流程图来逐行解析,首先前面进行空指针检查,
wonrkerCountOf()方法能够取得当前线程池中的线程的总数,取得当前线程数与核心池大小比较,
- 如果小于,将通过addWorker()方法调度执行。
- 如果大于核心池大小,那么就提交到等待队列。
- 如果进入等待队列失败,则会将任务直接提交给线程池。
- 如果线程数达到最大线程数,那么就提交失败,执行拒绝策略。
任务缓存队列
在前面我们多次提到了任务缓存队列,即workQueue,它用来存放等待执行的任务。
workQueue的类型为BlockingQueue
1)有界任务队列ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;
2)无界任务队列LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
3)直接提交队列synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。
拒绝策略
AbortPolicy:丢弃任务并抛出RejectedExecutionException
CallerRunsPolicy:由调用线程处理该任务。只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能会急剧下降。
DiscardOldestPolicy:丢弃队列中最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。
DiscardPolicy:丢弃任务,不做任何处理。
线程池的任务处理策略:
如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建一个线程去执行这个任务;
如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中,若添加成功,则该任务会等待空闲线程将其取出去执行;若添加失败(一般来说是任务缓存队列已满),则会尝试创建新的线程去执行这个任务;如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;
如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。
线程池的关闭
ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown()和shutdownNow(),其中:
shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务
六、四种常见的线程池
newFixedThreadPool
1 | public static ExecutorService newFixedThreadPool(int var0) { |
固定大小的线程池,可以指定线程池的大小,该线程池corePoolSize和maximumPoolSize相等,阻塞队列使用的是LinkedBlockingQueue,大小为整数最大值。
该线程池中的线程数量始终不变,当有新任务提交时,线程池中有空闲线程则会立即执行,如果没有,则会暂存到阻塞队列。对于固定大小的线程池,不存在线程数量的变化。同时使用无界的LinkedBlockingQueue来存放执行的任务。当任务提交十分频繁的时候,LinkedBlockingQueue迅速增大,存在着耗尽系统资源的问题。而且在线程池空闲时,即线程池中没有可运行任务时,它也不会释放工作线程,还会占用一定的系统资源,需要shutdown。
newSingleThreadExecutor
1 | public static ExecutorService newSingleThreadExecutor() { |
单个线程线程池,只有一个线程的线程池,阻塞队列使用的是LinkedBlockingQueue,若有多余的任务提交到线程池中,则会被暂存到阻塞队列,待空闲时再去执行。按照先入先出的顺序执行任务。
newCachedThreadPool
1 | public static ExecutorService newCachedThreadPool() { |
缓存线程池,缓存的线程默认存活60秒。线程的核心池corePoolSize大小为0,线程池最大为Integer.MAX_VALUE,阻塞队列使用的是SynchronousQueue。是一个直接提交的阻塞队列,他总会迫使线程池增加新的线程去执行新的任务。在没有任务执行时,当线程的空闲时间超过keepAliveTime(60秒),则工作线程将会终止被回收,当提交新任务时,如果没有空闲线程,则创建新线程执行任务,会导致一定的系统开销。如果同时又大量任务被提交,而且任务执行的时间不是特别快,那么线程池便会新增出等量的线程池处理任务,这很可能会很快耗尽系统的资源。
newScheduledThreadPool
1 | public static ScheduledExecutorService newScheduledThreadPool(int var0) { |
定时线程池,大小无限,此线程池支持定时以及周期性执行任务的需求,该线程池可用于周期性地去执行任务,通常用于周期性的同步数据。
scheduleAtFixedRate:是以固定的频率去执行任务,周期是指每次执行任务成功执行之间的间隔。
schedultWithFixedDelay:是以固定的延时去执行任务,延时是指上一次执行成功之后和下一次开始执行的之前的时间。