线程池的使用-《java并发编程实战》
在任务与执行策略之间的隐性耦合
Executor框架可以将任务的提交与任务的执行策略解耦开来。虽然Executor框架为定制和修改执行策略都提供了相当大的灵活性,但并非所有的任务都能适用所有的执行策略。有些类型的任务需要明确地指定执行策略,包括:
依赖性任务
使用线程封闭机制的任务
对响应时间敏感的任务
使用ThreadLocal的任务
在一些任务中,需要拥有或排除某种特定的执行策略。如果某些任务依赖于其他的任务,那么会要求线程池足够大,从而确保它们依赖任务不会被放入等待队列中或者被拒绝,而采用线程封闭机制的任务需要串行执行。通过将这些需求写入文档,将来的代码维护人员就不会由于使用了某种不合适的执行策略而破坏安全性或活跃性。
1)线程饥饿死锁
在线程池中没如果任务依赖于其他任务,那么可能产生死锁。饥饿死锁现象,只要线程池中的任务需要无限期地等待一些必须由池中其他任务才能提供的资源或条件而导致的死锁。
每当提交了一个有依赖性的Executor任务时,要清楚地知道可能会出现线程“饥饿”死锁,因此需要在代码或者配置Executor的配置文件中记录线程池的大小限制或配置限制。
2)运行时间过长的任务
如果任务阻塞的时间过长,那么即使不出现死锁,线程池的响应性也会变得糟糕。执行时间较长的任务不仅会造成线程池阻塞,甚至还会增加执行时间较短任务的服务时间。
有一项技术可以缓解执行事件较长任务造成的影响,即限定任务等待资源的时间,而不要无限制的等待。在平台类库的大多数可阻塞方法中,都同时定义了限时版本和无限时版本,例如Thread.join、BlockingQueue.put、CountDownLatch.await以及Selector.select等。如果等待超时,那么可以把任务标识为失败,然后中止任务或者将任务重新放回队列以便随后执行。这样,无论任务的最终结果是否成功,这种办法都能确保任务总能继续执行下去,并将线程释放出来以执行一些能更快完成的任务。如果在线程池中总是充满了被阻塞的任务,那么也可能表明线程池的规模过小。
设置线程大小
线程池的理想大小取决于被提交任务的类型以及所部署系统的特征。在代码中通常不会固定线程池的大小,而应该通过某种配置机制来提供,或者根据Runtime.availableProcessors来动态计算。
对于计算密集型的任务,在拥有N个处理器的系统上,当线程池的大小为N+1时,通常能实现最优的利用率。对于包含I/O操作或者其他阻塞操作的任务,由于线程并不会一直执行,因此线程池的规模应该更大。要正确地设置线程池的大小,你必须估算出任务的等待事件与计算事件的比值。这种估算不需要很精确,并且可以通过一些分析或监控工具来获得。
可以通过Runtime来获得CPU的数目:
int N_CPUS = Runtime.getRuntime().availableProcessors();

配置ThreadPoolExecutor
ThreadPoolExecutor为一些Executor提供了几本的实现,这些Executor是由Executors中的newCachedThreadPool等工厂方法返回的。ThreadPoolExecutor是一个灵活的、稳定的线程池,允许进行各种定制。
如果默认的执行策略不能满足需求,那么可以通过ThreadPoolExecutor的构造函数来实例化一个对象,并根据自己的需求来定制。
ThreadPoolExecutor的通用构造函数:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) {...}
1)线程的创建与销毁
线程池的基本大小(Core Pool Size)、最大大小(Maximum Pool Size)以及存活时间等因素共同负责线程的创建与销毁。基本大小也就是线程池的目标大小,即在没有任务执行时线程池的大小,并且只有在工作队列满了的情况下才会创建超出这个数量的线程。线程池的最大大小表示可同时活动的线程数量的上限。如果某个线程的空闲时间超过了存活时间,那么将标记为可回收的,并且当线程池的当前大小超过了几本大小时,这个线程将被终止。
newFixedThreadPool工厂方法将线程池的几本大小和最大大小设置为参数中指定的值,而且创建的线程池不会超时。newCachedThreadPool工厂方法将线程池的最大大小设置为Integer.MAX_VALUE,而将基本大小设置为零,并将超时设置为1分钟,这种方法创建出来的线程池可以被无线扩展,并且当需求降低时会自动收缩。
2)管理队列任务
在有限的线程池中会限制可并发执行的任务数量。
前面介绍,如果无限制地创建线程,那么将导致不稳定性,并通过采用固定大小的线程池来解决这个问题。然而,这个方案并不完整,在高负载的情况下,应用程序仍可能耗尽资源,只是出现问题的概率较小。如果新请求的到达速率超过了线程池的处理速率,那么新到来的请求将累积起来。在线程池中,这些请求会在一个有Executor管理的Runnable队列中等待,而不会像线程那样去竞争CPU资源。通过一个Runnable和一个链表节点来表现一个等待中的任务,当然比使用线程来表示的开销低很多,但如果客户提交给服务器请求的速率超过了服务器的处理速率,那么仍可能会耗尽资源。
ThreadPoolExecutor允许提供一个BlockingQueue来保存等待执行的任务。基本的任务排列方法有三种:无界队列、有界队列和同步移交。队列的选择与其他配置参数有关,例如线程池的大小等。
newFixedThreadPool和newSingleThreadExecutor在默认情况下将使用一个无界的LinkedBlockingQueue。一种更稳妥的资源管理策略是使用有界策略。例如ArrayBlockingQueue,有界队列有助于避免资源耗尽的情况发生,但它又带来了新问题,当队列填满后,新任务怎么办?
对于非常大的或者无界的线程池,可以使用SynchronousQueue不是一个真正的队列,而是一种在线程之间进行移交的机制。要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接受这个元素。如果没有线程正在等待,并且线程池的当前大小小于最大值,那么ThreadPoolExecutor将创建一个新的线程,否则根据饱和策略,这个任务将被拒绝。使用直接移交将更高效,因为任务会直接移交给执行它的线程,而不是被首先放在队列中,然后由工作者线程从队列中提取该任务。只有当线程池是无界的或者可以拒绝任务时,SynchronousQueue才有实际价值。在newCachedTheadPool工厂方法中就使用了SynchronousQueue。
当使用像LinkedBlockingQueue或ArrayBlockingQueue这样的FIFO队列时,任务的执行顺序与它们的到达顺序相同。如果想进一步控制任务执行顺序,还可以使用PriorityBlockingQueue,这个队列将根据优先级来安排任务。
对于Executor,newCachedThreadPool工厂方法是一种很好的默认选择,它能提供比固定大小的线程池更好的派对性能。当需要限制当前任务的数量以满足资源管理需求时,那么可以选择固定大小的线程池,就像在接受网络客户请求的服务器应用程序中,如果不进行限制,那么很容易发生过载问题。
只有任务相互独立时,为线程池或者工作队列设置界限才是合理的。如果任务之间存在依赖性,那么有界的线程池或队列就可能导致线程“饥饿”死锁问题。
3)饱和策略
当有界队列被填满后,饱和策略开始发挥作用。ThreadPoolExecutor的饱和策略可以通过调用setRejectedExecutionHandler来修改。(如果某个任务被提交到一个已被关闭的Executor时,也会用到饱和策略)。JDK提供了几种不同的RejectedExecutionHandler实现,每种实现都包含有不同的饱和策略:AbortPolicy、CallerRunsPolicy、DiscardPolicyPolicy和DiscardOldestPolicy。
中止Abort策略是默认的饱和策略,该策略将抛出未检查的RejectedExecutionException。抛弃Discard策略会悄悄抛弃该任务,抛弃最旧的Discard-Oldest策则会抛弃下一个江北执行的任务,然后尝试重新提交新的任务。(如果工作队列是一个优先队列,那么“抛弃最旧的”策略将的宗旨抛弃优先级最高的任务,因此最好不要将“抛弃最旧的饱和策略和优先级脆裂放在一起使用”);调用者运行Caller-Runs策略实现了一种调节机制,该策略既不会抛弃任务,也不会排除异常,而是将某些任务回退到调用者,从而降低新任务的流量。它不会在线程池的某个线程中执行新提交的任务,而是在一个调用了execute的线程中执行该任务。我们可以将WebServer示例又该为使用有界队列和“调用者运行”饱和策略,当线程池的所有线程都被占用,而且工作队列被填满后,下一个任务会在调用execute时在主线程中执行。由于执行任务需要一定的时间,因此主线程至少在一段时间内不能提交任何任务,从而使得工作者线程有时间来处理正在执行的任务。在这期间,主线程不会调用accept,因此到达的请求将被保存在TCP层的队列而不是在应用程序的队列中。如果持续过载,那么TCP层将最终发现它的请求队列被填满,因此同样会开始抛弃请求。当服务器跪在时,这种过载情况会逐渐向外蔓延开来,从线程池到工作队列到应用程序再到TCP层,最终达到客户端,导致服务器在高负载下实现一种平缓的性能降低。
4)线程工厂
每当线程池需要创建一个线程时,都是通过线程工厂方法来完成的。
ThreadFactory接口:
public interface ThreadFactory{
Thread newThread(Runnable r);
}
如果在应用程序中需要利用安全策略来控制对某些特殊代码库的访问权限,那么可以通过Executor中的privilegedThreadFactory工厂来定制自己的额线程工程。通过这种方式创建出来的线程,将与创建privilegedThreadFactory的线程拥有相同的访问权限、AccessControlContext和contextClassLoader。如果不适用privilegedThreadFactory,线程池创建的线程将从在需要新线程时调用execute或submit的客户程序中继承访问权限,从而导致令人困惑的安全性异常。
5)在调用构造函数后再定制ThreadPoolExecutor
在调用完ThreadPoolExecutor的构造函数后,仍然可以通过设置函数来修改大多数传递给它的构造函数的参数。如果Executor是通过Executors中的某个(newSingleThreadExecutor除外)工厂方法创建的,那么可以将结果的类型转换为ThreadPoolExecutor以访问设置器。
在Executors中包含一个unconfigurableExecutorService工厂方法,该方法对一个现有的ExecutorService进行包装,使其只暴露出ExecutorService的方法,因此不能对它进行配置。newSingleThreadExecutor返回的按这种方式封装的ExecutorService,而不是最初的ThreadPoolExecutor。可以使用这项技术防止执行策略被修改。
扩展ThreadExecutor
ThreadPoolExecutor是可拓展的,它提供了几个可以在子类化中改写的方法:beforeExecute、afterExecute和rerminated,这些方法可以用于扩展ThreadPoolExecutor的行为。
递归算法的并行化

浙公网安备 33010602011771号