8.1 在任务与执行策略之间的隐性耦合
有些类型的任务需要明确地指定执行策略,包括:
- 1.依赖性任务:避免产生活跃性问题
- 2.使用线程封闭机制的任务:任务要求其执行所在的Executor是单线程的。
- 3.对响应时间敏感的任务
- 4.使用ThreadLocal的任务
线程池的线程中不应该使用ThreadLocal在任务之间传递值。只有当线程本地值的生命周期受限于任务的生命周期时,在线程池的线程中使用ThreadLocal才有意义。
8.1.1 线程饥饿死锁
在线程池中,如果任务依赖于其他任务,那么可能产行死锁。
在线程池中,如果所有正在执行任务的线程都由于等待其他仍处于工作队列中的任务而阻塞,也会产生死锁。
每当提交了一个有依赖性的Executor任务时,要清楚地知道可能会出现线程饥饿死锁,因此需要在代码或配置Executor的配置文件中记录线程池的大小限制或配置限制。
8.1.2 运行时间较长的任务
限定任务等待资源的时间,如果等待超时,那么可以把任务标识为失败,然后中止任务或者将任务先更新放回队列以便随后执行。这样无论任务的最终结果是否成功,这种办法都能确保任务总能继续执行下去,并将线程释放出来执行一些能更快完成的任务。
8.2 设置线程池的大小
Runtime.availabeProcessors来动态计算。
过大:大量的线程将在相对很少的CPU和内存资源上发生竞争,导致更高的内存使用量或者耗尽资源。
过小:导致许多空闲的处理器无法执行工作 ,从而降低吞吐率。
要正确地设置线程池的大小,必须分析计算环境、资源预算和任务的特性。在部署的系统中有多少个CPU,多大的内存,任务是计算密集型还是I/O密集型。
对于计算密集型的任务,在拥有N个CPU的处理器上,当线程池的大小为N+1时,通常能实现最优的利用率。(即当计算密集型的线程偶尔由于页缺失故障或者其他原因而暂停时,这个额外的线程也能确保CPU时钟周期不会被浪费。)
对于包含I/O操作或者其它阻塞操作时,同埋线程不会一直执行,因此线程池的规模应该更大。要正确地设置线程池的大小,你必须 估算出任务的等待时间与计算时间的比值。
计算每个任务对该资源的需求量,然后用该资源的可用资源除以每个任务的需求量,就是线程池大小的上限。
8.3 配置ThreadPoolExecutor
ThreadPoolExecutor为一些Executor提供了基本的实现,这些Executor是由Executors中的newCachedThreadPool、newFixedThreadPool和newScheduledThreadExecutor等工厂方法返回的。它是一个灵活的、稳定的线程池,允许进行各种定制。
8.3.1 线程的创建与销毁
线程池的基本大小(Core Pool Size)、最大大小(Maxminum Pool Size)以及存活时间等因素共同负责线程的创建与销毁。基本大小也就是线程池的目标大小,即在没有任务执行时线程池的大小,并且只有在工作队列满了的情况下才会创建超出这个数量的线程。线程池的最大大小表示可同时活动的线程数量的上限。如果某个线程的空闲时间超过了存活时间,那么将被标记为可回收的,并且当线程池的当前大小超过了基本大小时,这个线程将被终止。
通过调节线程池的基本大小和存活时间,可以帮助线程池加以空闲线程占有的资源,从而使得这些资源可以用于执行任务。当需求增加时,必须创建新的线程来满足需求。
newFixedThreadPool工厂方法将线程池的基本大小和最大大小设置为参数中指定的值,而且创建的线程不会超时。
newCachedThreadPool工厂方法将线程池的最大大小设置为Integer.MAX_VALUE,而将基本大小设置为0,并将超时时间设置为1分钟,这种方法创建出来的线程池可以被无限扩展,并且当需求降低时会自动收缩。
corePoolSize最好不要等为0,因为只有当队列满的情况下才会创建新的线程来执行任务,所以提交的任务可以就一直不会执行
8.3.2 管理队列任务
无限制的创建线程,那么将导致不稳定性。通过固定大小的线程池来解决这个问题。然而这个方案在高负载的情况下,应用程序仍可能耗尽资源,只是出现问题的概率较小。如果客户提交给服务器请求的速度超过了服务器的处理速率,那么仍可能耗尽资源。甚至在耗尽内存子前,响应性能也会越来越差。
ThreadPoolExecutor允许提供一个BlockingQueue来保存等待执行的任务。基本的任务排队方式有3种:无界队列、有界队列和同步移次。
newFixedThreadPool和newSingleThreadExecutor使用一个无界的LinkedBlockingQueue。如果所有的工作者线程忙碌,那么队列将无限制地增加。
有界队列:ArrayBlockingQueue、有界的LinkedBlockingQueue、PriorityBlockingQueue。有界队列避免资源耗尽的情况,但队列满了这后,新的任务怎么办?如果线程池较小而队列较大,那么有助于减少内存使用量,降低CPU的使用率,同时还可以减少上下文切换,但付出的代价是可能会限制吞吐量。
对于非常大的或者无界的线程池,可以通过使用SynchronousQueue来避免任务排队,以及直接将任务从生产者移交给工作者线程。它是一种线程之间进行移交的机制 。要将一个元素放SynchronousQueue必须有另一个线程正在等待接受这个元素。如果没有线程正在等待,并且线程池的当前大小小于最大值,那么ThreadPoolExecutor将创建一个新的线程,否则根据饱和策略,这个任务将被拒绝。高效,只在当线程池是无界或者可以拒绝任务时,SynchronousQueue才有实际价值。在newCachedThreadPool工厂方法中就使用了SynchronousQueue。
当使用LinkedBlockingQueue或ArrayBlockingQueue这样的FIFO队列时,任务的执行顺序与它们的到达顺序相同。如果想进一步控制任务执行顺序,还可以使用PriorityBlockingQueue,这个队列将根据优先级来安排任务。优先级通过自然顺序或Comparator来定义。
对于Executor,newCachedThreadPool工厂方法是一种很好的默认选择,它能提供比固定大小的线程池更好的排队性能。当需要限制当前任务的数量以满足资源管理的需求时,那么选择固定大小的线程池,不会发生过载问题。
只有当任务相互独立时,为线程池或工作队列设置界限才是合理的。如果任务之间存在依赖性,那么有界的线程池或队列就可能导致线程饥饿死锁问题,应该使用newCachedThreadPool。
8.3.3 饱合策略
当有界队列被填满后,饱和策略开始发挥作用。ThreadPoolExecutor的饱和策略可以通过调用setRejectedExecutionHandler来处理。JDK提供了几种不同的实现:AbortPolicy、CallerRunsPolicy、DiscardPolicy和DiscardOldestPolicy。
AbortPolicy是默认的,它会抛出未检查的RejectedExecutionException。
Discard抛弃策略会悄悄抛弃该任务。
Discard-Oldest抛弃最旧的策略则会抛弃下一个将被执行的任务,然后尝试先更新提交新的任务。(如果工作队列是优先队列,将抛弃优先级最高的任务)
CallerRunsPolicy调用者运行策略实现了一种调节机制,会将任务回那到调用者,在调用者线程中执行该任务,由于执行任务会占用一定时间,因此一段时间内不能提交任何任务,从而使得工作线程有时间处理完正在执行的任务。在这期间,主线程不会调用accept,因此请求会保存在TCP层的队列中而不是在应用程序的队列中。如果持续过载,那么TCP层将最终发现它的请求队列填满,开始抛弃请求。
使用Semaphore来控制任务的提交速率。
8.3.4 线程工厂
线程池在创建一个线程时,都是通过线程工厂方法来完成的。默认的线程工厂方法将创建一个新的、非守护的线程,并且不包含特殊的配置信息。
定制的线程工厂方法,指定一个UncaughtExceptionHandler,或者实例化一个定制的Thread类、修改线程优先级、守护状态、名称,维护一些统计信息。
安全策略:privilegedThreadFactory
8.3.5 在调用构造函数后再定制ThreadPoolExecutor
如果Executor是通过Executors中的某个(newFixedThreadExecutor除外)工厂方法创建的,那么可以将结果的类型转换为ThreadPoolExecutor以访问设置器。
在Executors中包含一个unconfigurableExecutorService工厂方法,该方法对一个现有的ExecutorService进行包装,使其只暴露出ExecutorService的方法,因此不能对它进行配置。newSingleThreadExecutor返回按这种方式封闭的ExecutorService,而不是最初的ExecutorPoolExecutor。它需要确保不会被并发地执行任务。
8.4 扩展ThreadPoolExecutor
ThreadPoolExecutor提供了beforeExecutor、afterExecutor和terminated用于扩展行为。
在执行任务线程中将调用beforeExecute和afterExecute方法,在这些方法中可以添加日志、计时、收集统计信息等功能。无论任务是正常返回还是抛出一个异常返回,afterExecution都会被调用。如果任务带有Error,那么就不会调用afterExecution。如果beforeExecute抛出一个异常,那么任务也不会被执行。
在线程池完成关闭操作时调用terminated,也就是在所有任务都已经完成并且所有工作线程都关闭后。terminated可以用来释放Executor在其生命周期分配的各种资源,此外还可以执行发送通知、记录日志等操作。