线程池的使用

在任务与执行策略之间的隐形耦合

虽然Executor框架为制定和修改执行策略都提供了很大的灵活性,但并非所有的任务都适用所有的执行策略。有些类型的任务需要明确制定执行策略:

  • 依赖性任务
  • 使用线程封闭机制的任务:必须单线程
  • 对响应时间敏感的任务
  • 适用ThreadLocal的任务

线程饥饿死锁

只要线程池中的任务需要无限期地等待一些必须由池中其他任务才能提供的资源或条件,例如某个任务等待另一个任务的返回值或执行效果,那么除非线程池足够大,否则将发生线程饥饿死锁。

运行时间较长的任务

执行时间较长的任务不仅会造成线程阻塞,甚至会增加执行时间较短任务的服务时间。如果线程池中线程的数量远小于在稳定状态下执行时间较长任务的数量,那么到最后可能所有的线程都会运行这些执行时间较长的任务,从而影响整体的响应性。

设置线程池的大小

对于计算密集型的任务,在拥有N个处理器的系统上,当线程池大小为N+1时,能实现最优利用率。

对于包含I/O操作或其他阻塞操作时,由于线程不会一直执行,因此线程池的规模应该更大:

线程池大小=CPU数量目标CPU利用率(1+等待时间与运算时间的比率)

当然,CPU周期并不是唯一影响线程池大小的资源,还包括内存、文件句柄、套接字句柄和数据库连接等。计算每个任务对该资源的需求总量,然后用该资源的可用总量,除以每个任务的需求量,所得结果就是线程池大小的上限。

配置ThreadPoolExecutor

ThreadPoolExecutor的通用构造函数

1
2
3
4
5
6
7
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {}

线程池的创建与销毁

  1. 基本大小coolPoolSize:在没有任务执行时线程池的大小,并且只有在工作队列满了的情况下才会创建超出这个数量的线程。
  2. 最大大小maximumPoolSize:可同时活动的线程数量的上限。
  3. 存活时间keepAliveTime:如果某个线程的空闲时间超过了存活时间,那么将被标记为可回收的,并且当线程池的当前大小超过了基本大小时,这个线程将被终止。

管理队列任务

ThreadPoolExecutor允许提供一个BlockingQueue来保存等待执行的任务。基本的任务排队方法有3种:无解队列、有界队列和同步移交(Synchronous HandOff)。

newFixedThreadPool、newSingleThreadExecutor在默认情况下使用一个无界的LinkedBlockingQueue。如果所有工作者线程都在忙碌,那么任务将在队列中等待,如果任务持续快速到达,队列将无限增加。

一种更稳妥的资源管理策略是使用有界队列,例如ArrayBlockingQueue、有界LinkedBlockingQueue、PriorityBlockingQueue。有界队列必须与饱和策略配合使用。

对于非常大的或者无界线程池,可以使用SynchronousQueue来避免排队,以及直接将任务从生产者移交到工作者线程。要将一个元素放入SynchronousQueue,必须有一个线程在等待接受这个任务,如果没有线程在等待,且线程数量没有达到上限,那么将创建新的线程,否则根据饱和策略拒绝任务。newCachedThreadPool使用了SynchronousQueue。

PriorityBlockingQueue将按照优先级来安排任务,任务的优先级是按照自然顺序或Comparator来定义的。

只有当任务相互独立,为线程或工作队列设置界限才是合理的。如果任务之间存在依赖性,那么有界可能导致线程饥饿死锁,此时应该使用无界线程池,如newCachedThreadPool。

饱和策略

  • 中止策略(AbortPolicy):默认的饱和策略,该策略抛出RejectedExecutionException,调用者可以catch这个异常,根据需求编写处理代码。
  • 抛弃策略(DiscardPolicy):悄悄地抛弃任务。
  • 抛弃最旧的策略(DiscardOldestPolicy):抛弃下一个将要执行的任务,尝试提交新任务。
  • 调用者运行策略(CallerRunsPolicy):不会在线程池中执行新任务,而是在调用了execute的线程中执行。

线程工厂

通过指定一个线程工厂方法,可以定制线程池的配置信息,如:

  • 指定线程名字
  • 设置自定义UncaughtExceptionHandler
  • 维护一些统计信息(如有多少个线程被创建、销毁)
  • 线程创建或者终止时打日志

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

可以调用ThreadPoolExecutor的setter方法来修改参数。

Executors包含一个unconfigurableExecutorService方法将其装饰为ExecutorService接口,以防止执行策略被不信任的代码所修改。

扩展ThreadPoolExecutor

可重写方法:beforeExecute, afterExecute, terminated