一、创建线程池
ThreadPoolExecutor有四个构造方法:
|
|
创建一个线程池时需要的参数有:
-
int corePoolSize
(核心线程池大小):该参数的作用上面的内容已经讲得很清楚了。如果调用了ThreadPoolExecutor的prestartAllCoreThreads()方法,线程池会提前创建并启动所有核心线程(core thread) -
int maximumPoolSize
(最大线程池大小):线程池允许创建的最大线程数。阻塞队列满了之后,就会去创建新线程,且创建之后线程数小于最大线程数,线程池才会去执行任务。但是如果阻塞队列使用的是无界队列,该参数就不会有效果,毕竟阻塞队列都是无界。 -
long keepAliveTime
(空闲线程的存活时间):线程池中的线程空闲之后,可以存活的时间,如果过了这个时间,线程就会被销毁。如果任务很多,每个任务执行时间比较短,可以相对的调大keepAliveTime,可以提高线程的利用率。 -
TimeUnit unit
(keepAliveTime的时间单位):时分秒,毫秒和微秒等时间单位。 -
BlockingQueue<Runnable> workQueue()
(任务队列,阻塞队列):用于保存等待执行的任务的阻塞队列。在jdk中有几个阻塞队列的实现:- ArrayBlockingQueue:基于数组实现的有界阻塞队列。按照先进先出(FIFO)的原则对进来的元素进行排序。
- LinkedBlockingQueue:基于链表实现的游街队列,也是拥有先进先出的队列特性。吞吐量高于ArrayBlockingQueue。
- SynchronoutQueue:同步队列,不存储元素的阻塞队列。每插入一个元素必须等到该元素被取出,否则插入操作会被一直阻塞,吞吐量同通常高于LinkedBlockingQueue。
- PriorityBlockingQueue:具有优先级的无限阻塞队列。
-
ThreadFactory threadFactory
(线程工厂):可以通过线程工厂给每个创建出来的线程设置名字。 -
RejectedExecutionHandler handler
(拒绝执行的处理器):当队列和线程池都已经满了,那么线程池必须采用一种决绝策略来处理还在被提交过来的新任务。默认使用的拒绝策略是AbortPolicy,该策略在处理新任务时会直接抛出异常。jdk中线程池框架提供了四种决绝策略:- AbortPolicy:直接抛出异常。
- CallerRunsPolicy:任务返回给调用者所在线程来执行。
- DiscardOldestPolicy:对其队列中最近的一个任务,并执行并执行当前任务。
- DiscardPolicy:不处理任务,直接丢弃。
如果这四种决绝策略不满足,可以实现RejectedExecutionHandler 接口自定自己的拒绝策略。
二、向线程池提交任务
往ThreadPoolExecutor中提交任务有两个方法:execute()和submit()。
它们两个的不同是execute()方法只接受Runnable类型并且不能返回结果。
submit()方法能够接受Runnable和Callable两种类型的参数,所以submit()方式是完全可以取代execute()方法。需要返回任务的执行结果之类的返回值,只能使用submit()方法。submit()方法可以返回Future类型的对象。可以通过该类型的get()方法得到返回值,该方法会阻塞当前线程直到结果返回,使用get(long timeout,TimeUnit unit)方法会阻塞,知道时间结束,如果超时不返回结果,会抛出错误。
|
|
三、线程池的关闭
可以通过调用ThreadPoolExecutor的shutdown或shutdownNow方法来关闭线程池。原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别,shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。
四、线程池参数配置解析
要合理地配置线程池,必须分析任务特性,一般从以下几个角度来分析:
- 任务的性质:CPU密集型任务、IO密集型任务和混合型任务。
- 任务的优先级:高、中和低。
- 任务的执行时间:长、中和短。
- 任务的依赖性:是否依赖其他系统资源,如数据库连接。
性质不同的任务可以用不同规模的线程池分开处理。
CPU密集型任务应配置尽可能小的线程,如配置cpu +1个线程的线程池。
由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如2*cpu 。
混合型的任务,如果可以拆分,将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐量将高于串行执行的吞吐量。如果这两个任务执行时间相差太大,则没必要进行分解。可以通Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。
优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先执行。如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行,造成饥饿现象。
执行时间不同的任务可以交给不同规模的线程池来处理,或者可以使用优先级PriorityBlockingQueue队列,让执行时间短的任务先执行。
例如依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,等待的时间越长,则CPU空闲时间就越长,那么线程数应该设置得越大,这样才能更好地利用CPU。
使用有界队列。有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点,比如几千。如果设置成无界队列,那么线程池的队列就会越来越多,有可能会撑满内存引发OOM,导致整个系统不可用。
五、监控线程池
如果在系统中大量地使用了线程池,有必要对线程池进行监控,这样在出现问题地时候,可以快速定位线程池的问题。具体可以通过线程池的参数进行监控:
-
taskCount:线程池需要执行的任务数量。
-
completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount。
-
largestPoolSize:线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是否曾经满过。如该数值等于maximumPoolSize,表示线程池曾经满过。
-
getPoolSize:线程池的线程数量。如果线程池不销毁的话,线程池里的线程不会自动销毁,所以这个大小只增不减。
-
getActiveCount:获取活动的线程数。
通过扩展线程池进行监控。可以通过继承线程池来自定义线程池,重写线程池的beforeExecute、afterExecute和terminated方法,也可以在任务执行前、执行后和线程池关闭前执行一些代码来进行监控。例如,监控任务的平均执行时间、最大执行时间和最小执行时间等。 这几个方法在线程池里是空方法。
ThreadPoolExecutor中的方法,这些方法还是空方法,可以对其进行扩展。
|
|