线程池主要解决以下两个问题:
1)提升性能:线程池能独立负责线程的创建、维护和分配。在执行大量异步任务时,可以不需要自己创建线程,而是将任务交给线程池去调度。线程池能尽可能使用空闲的线程去执行异步任务,最大限度地对已经创建地线程进行复用,使得性能提升明显。
2)线程管理:每个Java线程池会保持一些基本地线程统计信息,例如完成地任务数量、空闲时间等,以便对线程进行有效管理,使得能对所接收地异步任务进行高效调度。
1、Executors的4种快捷创建线程池的方法
Java通过Executors工厂类提供了4种快捷创建线程池的方法:
(1)newSingleThreadExecutor创建“单线程化线程池”
该方法用于创建一个“单线程化线程池”,也就是只有一个线程的线程池,所创建的线程池用唯一的工作线程来执行任务,使用此方法创建的线程池能保证所有任务按照指定顺序(如FIFO)执行。
该线程池有以下特点:
1)单线程化的线程池中的任务是按照提交的顺序执行的。
2)池中的唯一线程的存活时间是无限的。
3)当池中的唯一线程正繁忙时,新提交的任务实例会进入内部的阻塞队列中,并且其阻塞队列是无界的。
适用的场景是:任务按照提交次序,一个任务一个任务地逐个执行的场景。
(2)newFixedThreadPool创建“固定数量的线程池”
该方法用于创建一个“固定数量的线程池”,其唯一的参数用于设置池中线程的“固定数量”。
该线程池有以下特定:
1)如果线程池没有达到“固定数量”,每次提交一个任务线程池内就创建一个新线程,直到线程达到线程池固定的数量。
2)线程池的大小一旦达到“固定数量”就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
3)在接收异步任务的执行目标实例时,如果池中的所有线程均在繁忙状态,新任务会进入阻塞队列中(无界的阻塞队列)。
适用的场景是:需要任务长时间的场景。
注:内部使用无界队列来存放排队任务,当大量任务超过线程池最大容量需要处理时,队列无限增大,使服务器资源迅速耗尽。
(3)newCachedThreadPool创建“可缓存线程池”
该方法用于创建一个“可缓存线程池”,如果线程池内的某些线程无事可干成为空闲线程,“可缓存线程池”可灵活回收这些空闲线程。
该线程池有以下特点:
1)在接收新的异步任务target执行目标实例时,如果池内所有线程繁忙,此线程池就会添加新线程来处理任务。
2)此线程池不会对线程池大小进行限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。
3)如果部分线程空闲,也就是存量线程的数量超过了处理任务数量,就会回收空闲(60秒不执行任务)线程。
适用的场景是:需要快速处理突发性强、耗时较短的任务场景。
注:线程池没有最大线程数量限制,如果大量的异步任务执行目标实例同时提交,可能会因创建线程过多而导致资源耗尽。
(4)newScheduledThreadPool创建“可调度线程池”
该方法用于创建一个“可调度线程池”,即一个提供“延时”和“周期性”任务调度功能的ScheduledExecutorService类型的线程池。
适用的场景是:周期性地执行任务地场景。
2、Executors快捷创建线程池的潜在问题
(1)SingleThreadPool和FixedThreadPool
这两个工厂方法所创建的线程池,任务队列的长度都为Integer.MAX_VALUE,可能会堆积大量的任务,从而导致OOM。
(2)CachedThreadPool和ScheduledThreadPool
这两个工厂方法所创建的线程池允许创建的线程数量为Integer.MAX_VALUE,可能会导致创建大量的线程,从而导致OOM。
3、线程池的标准创建方式
大部分企业的开发规范都会禁止使用快捷方法去创建线程池,而是要求通过标准构造器ThreadPoolExecutor去构造线程池。Executors工厂类中创建线程池的快捷方法实际上就是调用ThreadPoolExecutor(定时任务使用ScheduledThreadPoolExecutor)线程池的构造方法完成的。ThreadPoolExecutor构造方法有多个重载版本,其中一个比较重要的构造器如下:
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数,即使线程空闲,也不会回收
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 线程最大空闲时长
TimeUnit unit, // 空闲时长单位
BlockingQueue<Runnable> workQueue, // 任务阻塞队列
ThreadFactory threadFactory, // 新线程的产生方式
RejectedExecutionHandler handler // 拒绝策略
)
4、线程池的任务调度流程
线程池的任务调度流程大致如下:
1)如果线程池中总的工作线程数量小于核心线程数量(corePoolSize),接收到新任务,执行器会优先创建一个核心的工作线程,而不是从线程队列中获取一个空闲线程。
2)如果线程池中总的工作线程数量等于核心线程数量,接收到新的任务将被加入任务阻塞队列中,直到任务阻塞队列满为止。
3)当完成一个任务的执行时,执行器总是优先从任务阻塞队列中获取下一个任务,并开始执行,一直到任务阻塞队列为空。
4)在核心线程数量已经用完、任务阻塞队列已经满了的情况下,如果线程池接收到新的任务,将会为新任务创建一个非核心的工作线程,并且立即开始执行新任务。
5)在核心线程数量已经用完、任务阻塞队列已经满了的情况下,一直会创建非核心的工作线程去执行新任务,直到线程池内的工作线程总数超出最大线程数(maximumPoolSize),当新任务过来时,会为新任务执行拒绝策略。
6)当非核心的工作线程任务完成后空闲时间超过线程最大空闲时长(keepAliveTime),会被线程池回收。
总体的线程池的任务调度流程如下图所示:
5、任务阻塞队列
任务阻塞队列(BlockingQueue)与普通队列相比有一个重要的特点:在任务阻塞队列为空时会阻塞当前线程的元素获取操作。
其比较常用的实现类有:
1)ArrayBlockingQueue:是一个数组实现的有界阻塞队列,队列中的元素按FIFO排序。ArrayBlockingQueue在创建时必须设置大小,接收的任务超出corePoolSize数量时,任务被缓存到该任务阻塞队列中。
2)LinkedBlockingQueue:是一个基于链表实现的阻塞队列,按FIFO排序任务,可以设置容量(有界队列),不设置容量则默认使用Integer.Max_VALUE作为容量(无界队列)。该队列的吞吐量高于ArrayBlockingQueue。快捷工厂方法Executors.newSingleThreadExecutor和newSingleThreadExecutor所创建的线程池使用此队列,并且都没有设置容量。
3)PriorityBlockingQueue:是具有优先级的无界队列。
4)DelayQueue:是一个无界阻塞延迟队列,底层基于PriorityBlockingQueue实现,队列中每个元素都有过期时间,当从队列获取元素(元素出队)时,只有已经过期的元素才会出队,队列头部的元素是过期最快的元素。快捷工厂方法Executors.newScheduledThreadPool所创建的线程池使用此队列。
5)SynchronousQueue:(同步队列)是一个不存储元素的任务阻塞队列,每个插入操作必须等到另一个线程的调用移除操作,否则插入操作一直处于阻塞状态,其吞吐量通常高于LinkedBlockingQueue。快捷工厂方法Executors.newCachedThreadPool所创建的线程池使用此队列。
6、线程池的拒绝策略
在线程池的任务缓存队列为有界队列(有容量限制的队列)的时候,如果队列满了,提交任务到线程池的时候就会被拒绝。总体来说,任务被拒绝有两种情况:
1)线程池已经被关闭。
2)工作队列已满且maximumPoolSize已满。
无论以上哪种情况任务被拒绝,线程池都会调用RejectedExecutionHandler实例的rejectedExecution方法。RejectedExecutionHandler是拒绝策略的接口,JUC为该接口提供了以下几种实现:
1)AbortPolicy:拒绝策略。使用该策略时,如果线程池队列满了,新任务就会被拒绝,并且抛出RejectedExecutionException异常。该策略是线程池默认的拒绝策略。
2)DiscardPolicy:抛弃策略。使用该策略时,如果线程池队列满了,新任务就会直接被丢掉,并且不会有任何异常抛出。
3)DiscardOldestPolicy:抛弃最老任务策略。抛弃最老任务策略,也就是说如果队列满了,就会将最早进入队列的任务抛弃,从队列中腾出空间,再尝试加入队列。因为队列是队尾进队头出,队头元素是最老的,所以每次都是移除队头元素后再尝试入队。
4)CallerRunsPolicy:调用者执行策略。在新任务被添加到线程池时,如果添加失败,那么提交任务线程会自己去执行该任务,不会使用线程池中的线程去执行新任务。
5)自定义策略。如果以上拒绝策略都不符合需求,那么可自定义一个拒绝策略,实现RejectedExecutionHandler接口的rejectedExecution方法即可。
7、向线程池提交任务的两种方式
向线程池提交任务地两种方式为:调用execute()方法和调用submit()方法。
两种方法的区别:
1)execute()方法只能接收Runnable类型的参数,而submit()方法可以接收Callable、Runnable两种类型的参数。
2)submit()提交任务后会有返回值,而execute()没有。
3)submit()方便Exception处理。
8、线程池的生命周期
线程池的5种状态具体如下:
1)RUNNING(运行状态):线程池创建之后的初始状态,这种状态下可以执行任务。
2)SHUTDOWN(关闭状态):该状态下线程池不再接受新任务,但是会将任务队列中的任务执行完毕。
3)STOP(停止状态):该状态下线程池不再接受新任务,也不会处理任务队列中的剩余任务,并且将会中断所有工作线程。
4)TIDYING(整理状态):该状态下所有任务都已终止或者处理完成,将会执行terminated()钩子方法。
5)TERMINATED(结束状态):执行完terminated()钩子方法之后的状态。
线程池的状态转换规则为:
1)线程池创建之后状态为RUNNING。
2)执行线程池的shutdown()实例方法,会使线程池状态从RUNNING转变为SHUTDOWN。
3)执行线程池的shutdownNow()实例方法,会使线程池状态从RUNNING转变为STOP。
4)当线程池处于SHUTDOWN状态时,执行其shutdownNow()方法会将其状态转变为STOP。
5)等待线程池的所有工作线程停止,工作队列清空之后,线程池状态会从STOP转变为TIDYING。
6)执行完terminated()钩子方法之后,线程池状态从TIDYING转变为TERMINATED。
线程池的状态转换规则如下图所示:
网友评论