为了避免系统频繁地创建和销毁线程,可以让创建的线程进行复用。就像数据库连接一样,为了避免每次数据库查询时都重新建立和销毁数据库连接,可以使用数据库连接池维护一些数据库连接,让它们长期保持在一个激活状态,当系统需要使用数据库时,并不是重新创建一个新的连接,而是从连接池中获得一个可用的连接即可,反之当连接需要关闭时并不是真的把连接关闭,而是将这个连接“还”给连接池即可。通过这种方式可以节约不少创建和销毁对象的时间。
线程池也是类似的概念。在使用线程池之后,创建线程变成了从线程池获得空闲线程,关闭线程变成了向线程池归还线程。
内置线程池
JDK并发包中提供了一套Executor线程池框架。Executors类为线程池工厂,通过Executors可以取得一个拥有特定功能的线程池。Executor框架提供了各种类型的线程池,主要有5种:
newFixedThreadPool():通过该工厂方法返回一个固定线程数量的线程池。线程池中的线程数量始终不变。当没有空闲线程时,新提交的任务则被暂存在一个任务队列中。
newSingleThreadExecutor():通过该方法返回一个只有一个线程的线程池。多余的任务被保存在任务队列中,待线程空闲,按先进先出的顺序执行队列中的任务。
newCachedThreadPool():通过该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,当所有线程都在工作时,又有新的任务提交,则会创建新的线程来处理任务。所有线程在当前任务执行完毕之后,将返回线程池进行复用。
newSingleThreadScheduledExecutor():通过该方法返回一个SheduledExecutorService对象,线程池大小为1。SheduledExecutorService接口在ExecutorService接口上扩展了在给定时间执行某任务的功能,如在某个固定时延之后执行,或者周期性执行某个任务。
newScheduledThreadPool():通过该方法返回一个SheduledExecutorService的对象,但该线程池可以指定线程数量。
线程池参数
虽然核心的几个线程池有着不同的功能特点,但其内部实现均使用了ThreadPoolExecutor实现,都是通过ThreadPoolExecutor类进行封装之后得到的,实际上就是在ThreadPoolExecutor构造函数中指定不同的参数来构造不同的线程池。ThreadPoolExecutor构造函数为:
public ThreadPoolExecutor(
int corePoolSize,
int maxnumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
)
七个参数含义如下:
corePoolSize:指定了线程池中的线程数量。
maxnumPoolSize:指定了线程池中的最大线程数量。
keepAliveTime:当线程池线程数量超过corePoolSize时,多余的空闲线程的存活时间,即超过corePoolSize的空闲线程在多长的时间内会被销毁。
unit: keepAliveTime的单位。
workQueue:任务队列,被提交但尚未被执行的任务。
threadFactory:线程工厂,用于创建线程,一般用默认的即可。
handler:拒绝策略,当任务太多来不及处理时拒绝执行的策略。
Executor框架提供的几种封装好的线程池实际上就是指定固定参数来实现的。在这些参数中,任务队列和拒绝策略需要重点说明。
参数任务队列指被提交但是未执行的队列任务,它是一个BlockingQueue接口的对象,用于存放Runnable对象。根据队列功能分类,在ThreadPoolExecutor的构造函数中可使用几种BlockingQueue。
(1) 直接提交的队列:该功能由SynchronizedQueue对象提供。SynchronizedQueue是一个特殊的阻塞队列。SynchronizedQueue没有容量,每一个插入操作都要等待一个相应的删除操作,反之每一个删除操作都需要等待对应的插入操作。使用SynchronizedQueue时提交的任务不会被真实的保存,而总是将新任务提交给线程执行,如果没有空闲的线程则尝试创建新的线程,如果线程数量达到最大值就执行拒绝策略。使用SynchronizedQueue队列通常要设置很大的maxnumPoolSize,否则很容易执行拒绝策略。可以当做大小为0的队列来理解。
(2) 有界的任务队列:有界的任务队列可以使用ArrayBlockingQueue实现。当使用有界的任务队列时,若有新的任务需要执行,如果线程池的实际线程数小于核心线程数,则有优先创建新的线程,若大于核心线程数,则会将新任务加入等待队列。若队列已满,无法加入则在总线程数不大于最大线程数的前提下,创建新的线程。若大于最大线程数,则执行拒绝策略。也就是说,有界队列仅当任务队列满时才可能将线程数提升到核心线程数只上,否则确保线程数维持在核心线程数大小。
(3) 无界任务队列:无界任务队列可以通过LinkedBlockingQueue类来实现。与有界队列相比,除非系统资源耗尽,否则无界队列不存在任务入队失败的情况。当有新的任务到来,系统的线程数小于核心线程数时线程池会生成新的线程执行任务,但当系统线程数大于核心线程数后,就不会继续增加。若后续有新的任务,则将任务放入无界队列中等待。
(4) 优先任务队列:优先任务队列是带有执行优先级的队列,通过PriorityBlockingQueue实现,可以控制任务的执行先后顺序,是一个特殊的无界队列。无论是有界队列还ArrayBlockingQueue还是未指定大小的无界队列LinkedBlockingQueue,都是按照先进先出算法处理任务的,而优先队列则可以根据任务自身的优先级顺序执行,在确保系统性能的同时,也能有很好的质量保证。
回过头看Executor框架提供了几种线程池,newFixedThreadPool()返回的固定大小线程池中核心线程数和最大线程数一样,并且使用了无界队列。因为对于固定大小的线程池来说,不存在线程数量的动态变化,所以最大线程数等于核心线程数。同时,使用无界队列存放无法立即执行的任务,当任务提交非常频繁时,队列可能迅速膨胀,从而耗尽系统资源。
newSingleThreadExecutor()返回的单线程线程池,是固定大小线程池的一种退化,只是简单的将线程池数量设置为1。
newCachedThreadExecutor()返回核心线程数为0,最大线程数为无穷大的线程池。使用直接提交队列SynchronizedQueue。
当Executor提供的线程池不满足使用场景时,则需要使用自定义线程池,选择合适的任务队列来作为缓冲。不同的并发队列对系统和性能的影响均不同。
ThreadPoolExecutor的最后一个参数指定了拒绝策略。也就是当线程池中的线程已经用完,无法继续为新任务服务,同时等待队列中已经排满,再也塞不下新任务了。这时就需要一套机制,合理地处理这个问题。
JDK内置了四种拒绝策略:
AbortPolicy:该策略会抛出异常,阻止系统正常工作。
CallerRunsPolicy:只要线程池未关闭,该策略直接在调用者线程中运行当前被丢弃的任务。显然这样做不会真的丢弃任务,但是任务提交线程的性能可能会急剧下降。
DiscardOledestPolicy:该策略将丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务。
DiscardPolicy:该策略默默丢弃无法处理的任务,不予任何处理。在允许任务丢失的情况下这是一种合适的方案。
以上内置的策略均实现了RejectedExecutionHandler接口,若仍无法满足实际需要,可以自己扩展RejectedExecutionHandler接口,通过复写rejectedExecution方法,来实现自己饿拒绝策略,如下所示。该策略将丢弃的信息进行打印。
public ThreadPoolExecutor(5,5,
0L,TimeUnit.MICROSECONDS,
new LinkedBlockingQueue<Runnable>(10),
Executors.defaultThreadFactory(),
new RejectedExecutionHandler() {
public void rejected(Runnable r, ThreadPoolExecutor executor) {
System.out.println(r.toString()+"is discard");
}
});
线程池中的线程通过倒数第二个参数,也就是线程工厂来生成,具体是通过线程工厂中的newThread方法来产生。可以通过覆写该方法,自定义线程池中的线程的生成方式。
线程池扩展
在有些情形下需要对线程池做一些扩展,比如监控每个任务的执行的开始和结束时间,或者其他一些自定义的增强功能。ThreadPoolExecutor提供了beforeExecute()、afterExecute()和terminated()三个接口对线程池进行控制。以beforeExecute()和afterExecute()为例,在ThreadPoolExecutor.Worker.runTask()方法里提供了这样的实现:
boolean ran = false;
beforeExecute(thread,task);
try{
task.run();
ran = true;
afterExecute(task,null);
++completedTasks;
} catch (RuntimeException ex) {
if (! ran) {
afterExecute(task,ex);
}
throw ex;
}
ThreadPoolExecutor实现中,提供了空的beforeExecute()和afterExecute()实现。在实际应用中,可以对其进行扩展来实现对线程池运行状态的跟踪,输出一些有用的调试信息。如下所示:
ExecutorService eService = new ThreadPoolExecutor(5,5,
0L,TimeUnit.MICROSECONDS,
new LinkedBlockingQueue<Runnable>(10),
Executors.defaultThreadFactory(),
new RejectedExecutionHandler() {
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
System.out.println(r.toString()+"is discard");
}
}) {
@Override
protected void beforeExecute(Thread thread ,Runnable r){
System.out.println("准备执行"+((MyTask)r).name);
}
@Override
protected void afterExecute(Runnable r,Throwable t){
System.out.println("执行完成"+((MyTask)r).name);
}
};
for(int i = 0 ; i < 10 ; i++){
MyTask task = new MyTask("TASK-GEYM"+ i);
eService.execute(task);
}
eService.shutdown();
上述代码扩展了原有的线程池,在匿名扩展类中实现了beforeExecute()和afterExecute()方法,用于记录一个任务的开始和结束。在任务提交完成后,用shutdown()方法关闭线程池,该方法不会立即暴力终止所有任务,它会等待所有任务执行完成后再关闭线程池。可以理解为shutdown()发送了一个关闭信号。在shutdown()方法执行完毕之后,这个线程池不能再接受其他新的任务。