线程池

为了避免系统频繁地创建和销毁线程,可以让创建的线程进行复用。就像数据库连接一样,为了避免每次数据库查询时都重新建立和销毁数据库连接,可以使用数据库连接池维护一些数据库连接,让它们长期保持在一个激活状态,当系统需要使用数据库时,并不是重新创建一个新的连接,而是从连接池中获得一个可用的连接即可,反之当连接需要关闭时并不是真的把连接关闭,而是将这个连接“还”给连接池即可。通过这种方式可以节约不少创建和销毁对象的时间。
线程池也是类似的概念。在使用线程池之后,创建线程变成了从线程池获得空闲线程,关闭线程变成了向线程池归还线程。


内置线程池

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()方法执行完毕之后,这个线程池不能再接受其他新的任务。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值