前言

本文JDK版本为1.8

为什么要用线程池

在执行一个异步任务或并发任务时,使用JDK提供多线程技术,通过直接new Thread()方法来创建新的线程,这样做的缺点如下:

  1. 线程的创建与销毁需要占用一定的内存和资源,频繁的创建不利于资源的利用。
  2. 没有对线程的数量加以限制,无限制的创建势必会引发堆内存溢出,从而引发OOM。

更好的解决方案是合理地使用线程池,线程池的优点如下:

  1. 降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗。
  2. 提高系统响应速度,当有任务到达时,无需等待新线程的创建便能立即执行。
  3. 方便线程并发数的管控,线程若是无限制的创建,不仅会额外消耗大量系统资源,更是占用过多资源而阻塞系统或oom等状况,从而降低系统的稳定性。线程池能有效管控线程,统一分配、调优,提供资源使用率。
  4. 更强大的功能,线程池提供了定时、定期以及可控线程数等功能的线程池,使用方便简单。

设计

类图

Java里面线程池的顶级接口是Executor,但是Executor并不是一个线程池,而是提供一种将”任务提交”与”任务如何运行”分离开来的机制。

线程池的类体系结构:

  • ExecutorService:真正的线程池接口。
  • ScheduledExecutorService:能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。
  • ThreadPoolExecutor:线程池,ExecutorService的默认实现。
  • ScheduledThreadPoolExecutor:继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。

Executors

JDK推荐使用Executors工厂类来创建线程池。

Executors类提供4个静态工厂方法:newCachedThreadPool()、newFixedThreadPool(int)、newSingleThreadExecutor和newScheduledThreadPool(int)。这些方法最终都是通过ThreadPoolExecutor类来完成的,这里强烈建议大家直接使用Executors类提供的便捷的工厂方法,能完成绝大多数的用户场景,当需要更细节地调整配置,需要先了解每一项参数的意义。

ThreadPoolExecutor

1
2
3
4
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}

创建线程池,在构造一个新的线程池时,必须满足下面的条件:

  • corePoolSize(线程池基本大小):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,才会根据是否存在空闲线程,来决定是否需要创建新的线程。除了利用提交新任务来创建和启动线程(按需构造),也可以通过prestartCoreThread()prestartAllCoreThreads()方法来提前启动线程池中的基本线程。
  • maximumPoolSize(线程池最大大小):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。
  • keepAliveTime(线程存活保持时间):默认情况下,当线程池的线程个数多于corePoolSize时,线程的空闲时间超过keepAliveTime则会终止。但只要keepAliveTime大于0,allowCoreThreadTimeOut(boolean)方法也可将此超时策略应用于核心线程。也可以使用setKeepAliveTime()动态地更改参数。
  • workQueue(任务队列):用于传输和保存等待执行任务的阻塞队列。可以使用此队列与线程池进行交互:
  1. 如果运行的线程数少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队。
  2. 如果运行的线程数等于或多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不添加新的线程。
  3. 如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。
  • threadFactory(线程工厂),默认为DefaultThreadFactory类:用于创建新线程的工厂类。由同一个threadFactory创建的线程,属于同一个ThreadGroup,创建的线程优先级都为Thread.NORM_PRIORITY,以及是非守护进程状态。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。
  • handler(线程饱和策略)不能为空:当线程池和队列都满了,则表明该线程池已达饱和状态。
  1. ThreadPoolExecutor.AbortPolicy:处理程序遭到拒绝,则直接抛出运行时异常 RejectedExecutionException。(默认策略)。
  2. ThreadPoolExecutor.CallerRunsPolicy:调用者所在线程来运行该任务,此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。
  3. ThreadPoolExecutor.DiscardPolicy:无法执行的任务将被删除。
  4. ThreadPoolExecutor.DiscardOldestPolicy:如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重新尝试执行任务(如果再次失败,则重复此过程)。

添加一个任务到线程池

  • 如果线程池中运行的线程小于corePoolSize ,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
  • 如果线程池中运行的线程大于等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列
  • 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满(即无法将请求加入队列 ),并且线程池中的数量小于maximumPoolSize,则创建新的线程来处理被添加的任务。
  • 如果此时线程池中的数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize ,那么通过 handler所指定的策略来处理此任务。
  • 当线程池中的线程数量大于corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,线程池可以动态的调整池中的线程数。

使用

需要针对具体情况而具体处理,不同的任务类别应采用不同规模的线程池,任务类别可划分为CPU密集型任务、IO密集型任务和混合型任务。

  • 对于CPU密集型任务:就需要尽量压榨CPU,参考值可以设为 NCPU+1。
  • 对于IO密集型任务:由于IO操作速度远低于CPU速度,那么在运行这类任务时,CPU绝大多数时间处于空闲状态,那么线程池可以配置尽量多些的线程,以提高CPU利用率,参考值可以设置为2*NCPU。
  • 对于混合型任务:可以拆分为CPU密集型任务和IO密集型任务,当这两类任务执行时间相差无几时,通过拆分再执行的吞吐率高于串行执行的吞吐率,但若这两类任务执行时间有数据级的差距,那么没有拆分的意义。