Java线程池在多线程编程时常用的工具,本文简要的介绍一下线程池的原理、任务队列、拒绝策略以及性能对比。

线程池工作原理

  1. 线程池刚创建时,未创建线程,任务队列是作为入参。
  2. 当调用 execute() 方法添加一个任务时,线程池会做如下判断:
    • 如果正在运行的线程数量小于 corePoolSize,则创建线程运行这个任务;
    • 如果正在运行的线程数量大于或等于 corePoolSize,则将这个任务放入队列。
    • 如果队列满了,且正在运行的线程数量小于 maximumPoolSize,则创建线程运行这个任务;
    • 如果队列满了,且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会拒绝任务,并根据相应的拒绝策略处理。
  3. 当一个线程完成任务时,它会从队列中取下一个任务来执行。
  4. 当一个线程空闲超过一定的时间(keepAliveTime)时,若当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

任务队列

任务队列与池交互

所有BlockingQueue都可用于传输和保持提交的任务。可以使用此队列与池大小进行交互:

  • 如果运行的线程少于 corePoolSize,则 Executor 始终首选添加新的线程,而不进行排队。
  • 如果运行的线程等于或多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不添加新的线程。
  • 如果无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。

队列策略

  1. 直接提交。工作队列的默认选项是 SynchronousQueue,它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
  2. 无界队列。使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize。(因此,maximumPoolSize 的值也就无效了。)当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web 页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
  3. 有界队列。当使用有限的 maximumPoolSizes 时,有界队列(如 ArrayBlockingQueue有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O 边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU 使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。

拒绝策略

JDK预定义

  1. 在默认的 ThreadPoolExecutor.AbortPolicy中,处理程序遭到拒绝将抛出运行时RejectedExecutionException
  2. ThreadPoolExecutor.CallerRunsPolicy中,线程调用运行该任务的execute 本身。此策略提供简单的反馈控制机制,能够减缓新任务的提交速度。
  3. ThreadPoolExecutor.DiscardPolicy中,不能执行的任务将被删除。
  4. ThreadPoolExecutor.DiscardOldestPolicy中,如果执行程序尚未关闭,则位于工作队列头部的任务将被删除,然后重试执行程序(如果再次失败,则重复此过程)。

自定义

可以自定义实现RejectedExecutionHandler 接口。

任务队列性能测试

测试环境

项目 参数
操作系统 macOS
CPU 2.6 GHz Intel Core i5
内存 8 GB 1600 MHz DDR3
硬盘 500G SSD

测试前提

并发请求数:500000,每10000次停顿10微秒;拒绝策略为ThreadPoolExecutor.AbortPolicy,即抛出异常;

测试结果

平均延时单位:μs

平均延时单位:μs

结论分析

  1. 由测试结果可得以下结论:
    • 若应用对请求延时要求比较高时,线程池建议使用同步队列,能够保证任务处理低延时;
    • 若应用对延时要求不高,且主机处理能力有限,则建议使用有界队列,能够提高吞吐量并保证较低失败率;
    • 若应用对延时要求不高,且主机处理能力较强,则建议加大线程池最大线程数,保证高吞吐低失败率;
  2. 往往应用需实现对线程池的监控,当长时间请求失败率较高时,则需要调整线程池参数。另线程池corePoolSize必须小于等于maximumPoolSize,为避免人为配置失误,造成系统不可用,corePoolSize、maximumPoolSize需相同,提供接口供动态调整。