JDK1.5中引入了强大的concurrent包,线程池的实现ThreadPoolExcutor。
线程池种类
通常开发者都是利用Executors提供的通用线程池创建方法,去创建不同配置的线程池,主要区别在于ExecutorService类型或者不同的初始参数。
Executors目前提供了五种不同的线程池创建配置:
- newCachedThreadPool(),它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过60秒,则被终止并移出缓存;长时间闲置时,这种线程池并不会消耗什么资源,其内部使用SynchronousQueue作为工作队列;
- newFixedThreadPool(),创建固定大小的线程池,背后使用的是无界的工作队列,任何时候最多有nThreads个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目nThreads。
- newSingleThreadExcuteor(),它的特点在于工作线程数目被限制为1,操作一个无界的工作队列,所以保证了所有任务都是被顺序执行的,最多只会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目。
- newSingleThreadScheduledExcutor()和newScheduleThreadPool(int corePoolSize),创建的是个ScheduledExcutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。
- newWorkStealingPool(int parallelism),这是一个经常被人忽略的线程池,Java 8才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序。
阻塞队列(runnableTaskQueue)
- ArrayBlockingQueue:一个基于数组结构的有界阻塞队列,此队列按照先进先出原则对元素进行排序;
- LinkedBlockingQueue:一个基于链表结构的阻塞队列,按照先进先出对元素进行排序。吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Excutors.newFixedThreadPool()使用了这个队列;
- SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态。吞吐量要高于LinkedBlockingQueue。静态工厂方法Excutors.newCachedThreadPool()使用了这个队列;
- PriorityBlockingQueue:一个具有优先级的无界阻塞队列。
系统负载
参数的设置跟系统的负载有直接的关系,系统负载相关参数:
- tasks,每秒需要处理的最大任务数
- tasktime,处理每个任务所需要的时间
- responsetime,系统允许任务最大的响应时间,比如每个任务的响应时间不得超过2秒
ThreadPoolExcutor类可设置的参数有:
1.corePoolSize:
核心线程数,核心线程会一直存活,即使没有任务需要处理。当线程数小于核心线程数时,即使现有的线程空闲,线程池也会优先创建新线程来处理任务,而不是直接交给现有的线程处理;
核心线程在allowCoreThreadTimeout被设置为true时会超时退出,默认情况下不会退出。
每个任务需要tasktime秒处理,则每个线程每秒可以处理1/tasktime个任务。系统每秒有tasks个任务,所以需要的线程数为tasks/(1/tasktime),即tasks*tasktime个线程数。
假设系统每秒任务数为100-1000,每个任务耗时0.1秒,那么需要10-100个线程, 核心线程数应设置为大于10,具体数字最好根据8020原则,即80%情况下系统每秒任务数,若系统80%情况下每秒任务数小于200,最多时为1000,那么可以设置为20。
2.maxPoolSize:
当线程数大于或等于核心线程,且任务队列已满时,线程池会创建新的线程,直到线程数量达到maxPoolSize。如果线程数等于最大线程数,则已经超出线程池的处理能力,线程池会拒绝处理任务而抛出异常。
当系统负载达到最大值的时候,核心线程数已经无法按时处理完所有任务,这时候就需要增加线程。每秒200个任务需要20个线程,当每秒达到1000个任务的时候,需要(1000-queueCapacity)*(20/200)=60个线程,可以将最大线程数设置为60。
3.keepAliveTime:
当线程空闲时间达到keepAliveTime,该线程会退出,直到线程数量达到corePoolSize。如果allowCoreThreadTimeout被设置为true,则所有线程均会退出直到线程数量为0.
4.allowCoreThreadTimeout:
是否允许核心线程空闲退出。
5.queueCapacity:
任务队列容量。从maxPoolSize的描述上可以看出,任务队列的容量会影响到线程的变化,因此任务队列的长度也需要恰当的设置。
任务队列的长度要根据核心线程数,以及系统对任务响应时间的要求有关。队列长度可以设置为:
(corePoolSize/tasktime)*responsetime:20/0.1*2=400,即队列长度可以设置为400。
队列长度设置过大,会导致任务响应时间过长,切忌以下写法:
LinkedBlockingQueue queue=new LinkedBlockingQueue();
这实际上是将队列长度设置为Integer.MAX_VALUE,将会导致线程数量永远为corePoolSize,再也不会增加,当任务数量陡增时,任务响应时间也随之陡增。
6.RejectedExcutionHandler:饱和策略
当队列和线程池都满了,说明线程池处于饱和状态,那么必须对新提交的任务采用一种特殊的策略来进行处理。这个策略默认配置是AbortPolicy,表示无法处理新的任务而抛出异常。JAVA提供了4中策略:
- AbortPolicy:直接抛出异常
- CallerRunsPolicy:只用调用所在的线程运行任务
- DiscardOledestPolicy:丢弃队列里最近的一个任务,并执行当前任务
- DiscardPolicy:不处理,丢弃掉
以上关于线程数量的计算并没有考虑CPU的情况。若结合CPU的情况,比如,当线程数量达到50时,CPU达到100%,则将maxPoolSize设置为60也不合适,此时若系统负载长时间维持在每秒1000个任务,则超出线程池处理能力,应设法降低每个任务的处理时间(tasktime)。