并发编程实战 - 线程池的使用

在标准的Executor实现中,当执行需求较低时将回收空闲线程,而当需求增加时将添加新的线程,并且如果从任务中抛出了未检查异常,那么将用一个新的工作者线程替代抛出异常的线程。只有当线程本地值的生命周期受限于任务的生命周期时,在线程池中的线程使用ThreadLocal才有意义,而在线程池的线程中不应该使用ThreadLocal在任务之间传递值。

只有当任务都是同类型的并且相互独立时,线程池的性能才能达到最佳。如果将运行时间较长的与运行时间较短的任务混合在一起,那么除非线程池很大,否则将可能造成“阻塞”。如果提交的任务依赖于其他任务,那么除非线程池无限大,否则可能造成死锁。幸运的是基于网络的典型服务器应用程序中 - 网页服务器、邮件服务器以及文件服务器等,它们的请求通常都是同类型的并且相互独立。如果在线程池中总是充满了被阻塞的任务,那么说明线程池的规模太小。

1、设置线程池的大小

在代码中通常不会固定线程池的大小,而是应该通过某种配置机制来提供,或者根据Runtime.availableProcessor来动态计算。如果需要执行不同类别的任务,并且它们之间的行为相差很大,那么应该考虑使用多个线程池,从而使每个线程池都可以根据各自的工作负载来调整。
对于计算密集型的任务,在拥有n个处理器的系统上,当线程池的大小是n+1时,通常能实现最优的利用率。对于包含I/O操作或者其他阻塞操作的任务,由于线程并不会一直执行,因此线程池的规模应该更大。
newFixedThreadPool工厂方法将线程池的基本大小和最大大小设置为参数中指定的值且创建的线程池不会超时。newCachedThreadPool工厂方法将线程池的最大大小设置为Integer.MAX_VALUE,而将基本大小设置为零,并将超时时间设置为1分钟,这种方法创建出来的线程池可以被无限扩展,并且当需求降低时会自动收缩。

2、线程池任务队列

ThreadPoolExecutor允许提供一个BlockingQueue来保存等待执行的任务。基本的任务排队方法有三种:无界队列、有界队列和同步移交。队列的选择与其他配置参数有关,例如线程池的大小。
newFixedThreadPool和newSingleThreadExecutor在默认情况下将使用一个无界的LinkedBlockingQueue。如果所有的工作者线程都处于忙碌状态,那么任务将在队列中等候。如果任务持续快速地到达,并且超过了线程池处理它们的速度,那么队列将无限制地增加。
一种更稳妥的资源管理策略是使用有界队列,例如ArrayBlockingQueue、有界的LinkedBlockingQueue、PriorityBlockingQueue。有界队列有助于避免资源耗尽的使用情况,但又带来了新的问题:如果队列填满了,新的任务怎么办? - 饱和策略。在使用有界的工作队列时,队列的大小与线程池的大小必须一起调节。
对于非常大的或者无界的线程池,可以通过SynchronousQueue来避免任务排队,以及直接将任务从生产者移交给工作者线程。SynchronousQueue不是一个真正的队列,而是一种在线程之间进行移交的机制。要将一个元素放入SynchronousQueue中,必须有另外一个线程正在等待接受这个元素。在newCachedThreadPool工厂方法中就使用了SynchronousQueue。
对于Executor,newCachedThreadPool工厂方法是一种很好的默认选择,它能提供比固定大小的线程池更好的排队性能。当需要限制当前任务的数量以及满足资源管理需求时,那么可以选择固定大小的线程池,就像在接受网络客户请求的服务器应用程序中,如果不进行限制,那么很容易发生过载的问题。
只有当任务相互独立时,为线程池或工作队列设置界限才是合理的。如果任务直接存在依赖性,那么有界的线程池或者队列就可能导致线程“饥饿”死锁问题。此时应该使用无界的线程池,例如newCachedThreadPool。

3、饱和策略

当有界队列被填满后,饱和策略开始发挥作用。ThreadPoolExecutor的饱和策略可以通过setRejectedExecutionHandler来修改。(如果一个任务被提交到一个已经关闭的Executor,也会用到饱和策略)JDK提供了几种不同的RejectedExecutionHandler实现,每种实现都包含有不同的饱和策略:AbortPolicy、CallerRunsPolicy、DiscardPolicy、DiscardOldestPolicy。
中止策略(abortPolicy):默认的饱和策略,该策略将抛出未检查的RejectedExecutionException。调用者可以捕获这个异常,然后根据需求编写自己的处理代码。
当新提交的任务无法保存到队列中等待执行时,抛弃策略DiscardPolicy会悄悄的抛弃该任务。
“抛弃最旧的”(DiscardOldestPolicy)则会抛弃下一个将被执行的任务,然后重新尝试提交新的任务。(如果工作队列是一个优先级队列,那么“抛弃最旧的”策略将导致抛弃优先级最高的任务,因此最好不要将“抛弃最旧的”饱和策略与优先级队列一起使用)。
“调用者运行”策略实现了一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退给调用者,从而降低新任务的流量。

4、线程工厂

每当线程池需要创建一个线程时,都是通过线程工厂方法来完成的。默认的线程工厂将创建一个新的、非守护的线程,并且不包括特殊的配置信息。通过指定一个线程工厂方法,可以定制线程池的配置信息。在ThreadFactory中只定义了一个方法newThread,每当线程池需要创建一个新线程时,都会调用这个方法。
在许多情况下都需要使用定制的线程工厂方法,例如为每个线程设置一个名字、设置一个UnCaughtExceptionHandler等,都可以通过自定义ThreadFactory来进行。
如果在应用程序中需要利用安全策略来控制对某些特殊代码库的访问权限,那么可以通过Executor中的privilegedThreadFactory工厂来定制自己的线程工厂。如果不使用privilegedThreadFactory,线程池创建的线程将从在需要新线程时调用execute或submit的客户程序中继承访问权限,从而导致令人困惑的安全问题。

5、在调用构造函数之后再定制ThreadPoolExecutor

在调用完ThreadPoolExecutor的构造函数之后,仍然可以通过设置函数(Setter)来修改大多数传递给它的构造函数的参数(例如线程池的大小、最大大小、存活时间、线程工厂以及拒绝执行处理器)。如果Executor是通过Executors中的某个(newSingleThreadExecutor除外)工厂方法创建的,那么可以将结果的类型转换为ThreadPoolExecutor以访问设置器。
在Executors中包含一个unconfigurableExecutorService工厂方法,该方法对一个现有的ExecutorService进行包装,使其只暴露出ExecutorService的方法,因此不能对它进行配置。newSingleThreadExecutor返回按这种方式封装的ExecutorService,而不是最初的ThreadPoolExecutor。如果在代码中增加了单线程Executor的线程池大小,那么将破坏它的执行语义。你可以在自己的Executor中使用这项技术以防止执行策略被修改。如果将ExecutorService暴露给不信任的代码,又不希望对其进行修改,就可以通过unconfigurableExecutorService来包装它。

6、扩展ThreadPoolExecutor

ThreadPoolExecutor是可扩展的,它提供了几个可以在子类中改写的方法:beforeExecute、afterExecute和terminated,这些方法可以用于扩展ThreadPoolExecutor的行为。
在执行任务的线程中将调用beforeExecute、afterExecute等方法,在这些方法中还可以添加日志、计时、监视或统计信息收集等功能。无论任务是从run正常返回还是抛出一个异常而返回,afterExecute都会被调用(如果任务完成后带有一个Error,那么就不会调用afterExecute)。如果beforeExecute抛出一个RuntimeException,那么任务将不会被执行,并且afterExecute也不会被调用。
在线程池完成关闭操作时调用terminated,也就是在所有任务都已经完成并且所有工作者线程也关闭后。terminated可以用来释放Executor再其生命周期里分配的各种资源,此外还可以执行发送通知、记录日志或者收集finalize统计信息等操作。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值